diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Common/Layout/LeftSubSidebar/LeftSubSidebar.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Common/Layout/LeftSubSidebar/LeftSubSidebar.tsx index f8cfca0f645..413b66a2008 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Common/Layout/LeftSubSidebar/LeftSubSidebar.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Common/Layout/LeftSubSidebar/LeftSubSidebar.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { Link } from 'react-router'; +import { browserHistory, Link } from 'react-router'; import { FaSearch } from 'react-icons/fa'; import { Button } from '@/new-components/Button'; import { Analytics } from '@/features/Analytics'; import styles from './LeftSubSidebar.module.scss'; +import { DropdownButton } from '@/new-components/DropdownButton'; interface Props extends React.ComponentProps<'div'> { showAddBtn: boolean; @@ -15,6 +16,11 @@ interface Props extends React.ComponentProps<'div'> { addTrackId: string; addTestString: string; childListTestString: string; + /* padding addBtn override the default "create" button + e.g. for action creation in pro console we pass the dropdown button to choose between + action form and import from OpenAPI + */ + addBtn?: React.ReactNode; } const LeftSubSidebar: React.FC = props => { @@ -28,6 +34,7 @@ const LeftSubSidebar: React.FC = props => { addTestString, children, childListTestString, + addBtn, } = props; const getAddButton = () => { @@ -71,7 +78,7 @@ const LeftSubSidebar: React.FC = props => { > {heading} - {getAddButton()} + {addBtn ?? getAddButton()}
    {children} diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Add/Add.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Add/Add.tsx index efc860e419a..3ea80124a9f 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Add/Add.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Add/Add.tsx @@ -45,7 +45,6 @@ import { ResponseTransformStateBody, } from '@/components/Common/ConfigureTransformation/stateDefaults'; import ConfigureTransformation from '@/components/Common/ConfigureTransformation/ConfigureTransformation'; -import { GeneratedAction, OasGeneratorModal } from '@/features/Actions'; import ActionEditor from '../Common/components/ActionEditor'; import { createAction } from '../ServerIO'; import { getActionDefinitionFromSdl } from '../../../../shared/utils/sdlUtils'; @@ -243,8 +242,6 @@ const AddAction: React.FC = ({ const responseBodyOnChange = (responseBody: ResponseTransformStateBody) => { responseTransformDispatch(setResponseBody(responseBody)); }; - const [isActionGeneratorOpen, setIsActionGeneratorOpen] = - React.useState(false); // we send separate requests for the `url` preview and `body` preview, as in case of error, // we will not be able to resolve if the error is with url or body transform, with the current state of `test_webhook_transform` api @@ -351,66 +348,6 @@ const AddAction: React.FC = ({ } } - const onImportGeneratedAction = (generatedAction: GeneratedAction): void => { - const { - types, - action, - description, - method, - baseUrl, - path, - requestTransforms, - responseTransforms, - headers: actionHeaders, - sampleInput, - queryParams, - } = generatedAction; - typeDefinitionOnChange(types, null, null, null); - actionDefinitionOnChange(action, null, null, null); - commentOnChange({ - target: { value: description }, - } as React.ChangeEvent); - handlerOnChange(baseUrl); - if (requestTransforms) { - requestPayloadTransformOnChange(true); - requestBodyOnChange({ - action: 'transform', - template: requestTransforms, - }); - } else { - requestPayloadTransformOnChange(false); - } - toggleForwardClientHeaders(); - if (responseTransforms) { - responsePayloadTransformOnChange(true); - responseBodyOnChange({ - action: 'transform', - template: responseTransforms, - }); - } else { - responsePayloadTransformOnChange(false); - } - - transformDispatch(setRequestSampleInput(sampleInput)); - requestMethodOnChange(method); - requestUrlTransformOnChange(true); - requestUrlOnChange(path.replace(/\{([^}]+)\}/g, '{{$body.input.$1}}')); - requestQueryParamsOnChange(queryParams); - setHeaders( - actionHeaders.map(name => ({ - name, - value: `{{$body.input.${name}}}`, - type: 'static', - })) - ); - - setTimeout(() => { - if (createActionRef.current) { - createActionRef.current.scrollIntoView(); - } - }, 0); - }; - return (
    @@ -418,13 +355,6 @@ const AddAction: React.FC = ({

    Add a new action

    - {isActionGeneratorOpen && ( - setIsActionGeneratorOpen(false)} - /> - )} - = ({ toggleForwardClientHeaders={toggleForwardClientHeaders} actionDefinitionOnChange={actionDefinitionOnChange} typeDefinitionOnChange={typeDefinitionOnChange} - onOpenActionGenerator={() => setIsActionGeneratorOpen(true)} /> , ast: Nullable> ) => void; - onOpenActionGenerator?: () => void; }; const ActionEditor: React.FC = ({ @@ -77,7 +73,6 @@ const ActionEditor: React.FC = ({ toggleForwardClientHeaders, actionDefinitionOnChange, typeDefinitionOnChange, - onOpenActionGenerator, }) => { const { sdl: typesDefinitionSdl, @@ -142,24 +137,6 @@ const ActionEditor: React.FC = ({ You can use the custom types already defined by you or define new types in the new types definition editor below.

    - {onOpenActionGenerator && - isProConsole(window.__env) && - (isImportFromOASEnabled ? ( -
    - - - -
    - ) : ( - - ))} + + )}

    {getIntroSection()} diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Router.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Router.js index 4672a819034..77841e3059c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Router.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Router.js @@ -12,6 +12,7 @@ import AddAction from './Add/Add'; import TypesManage from './Types/Manage'; import TypesRelationships from './Types/Relationships'; import { exportMetadata } from '../../../metadata/actions'; +import { OASGeneratorPage } from '@/features/Actions'; const actionsInit = ({ dispatch }) => { return (nextState, replaceState, cb) => { @@ -40,6 +41,7 @@ const getActionsRouter = (connect, store, composeOnEnterHooks) => { + { + const actionComment = rawState.comment ? rawState.comment.trim() : null; + + const { + name: actionName, + arguments: args, + outputType, + error: actionDefError, + type: actionType, + } = getActionDefinitionFromSdl(rawState.actionDefinition.sdl); + if (actionDefError) { + throw new Error('Invalid Action Definition', { + cause: actionDefError, + }); + } + + const { types, error: typeDefError } = getTypesFromSdl( + rawState.typeDefinition.sdl + ); + + if (typeDefError) { + throw new Error('Invalid Types Definition', { cause: typeDefError }); + } + + const state = { + handler: rawState.handler.trim(), + kind: rawState.kind, + types, + actionType, + name: actionName, + arguments: args, + outputType, + headers: rawState.headers, + comment: actionComment, + timeout: parseInt(rawState.timeout, 10), + forwardClientHeaders: rawState.forwardClientHeaders, + }; + + const validationError = getStateValidationError(state, existingTypesList); + if (validationError) { + throw new Error('Validation Error', { cause: validationError }); + } + + const typesWithRelationships = hydrateTypeRelationships( + state.types, + existingTypesList + ); + + const { types: mergedTypes, overlappingTypenames } = mergeCustomTypes( + typesWithRelationships, + existingTypesList + ); + + if (overlappingTypenames) { + const isOk = getOverlappingTypeConfirmation( + state.name, + allActions, + existingTypesList, + overlappingTypenames + ); + if (!isOk) { + return; + } + } + // Migration queries start + const migration = new Migration(); + + const customFieldsQueryUp = generateSetCustomTypesQuery( + reformCustomTypes(mergedTypes) + ); + + const customFieldsQueryDown = generateSetCustomTypesQuery( + reformCustomTypes(existingTypesList) + ); + migration.add(customFieldsQueryUp, customFieldsQueryDown); + const actionQueryUp = generateCreateActionQuery( + state.name, + generateActionDefinition(state, requestTransform, responseTransform), + actionComment + ); + + const actionQueryDown = generateDropActionQuery(state.name); + + migration.add(actionQueryUp, actionQueryDown); + // Migration queries end + return { name: actionName, migration }; +}; + +export const executeActionCreation = ( + dispatch, + getState, + name, + migration, + rawState, + redirect = true, + onSuccess, + onFail +) => { + const migrationName = `create_action_${name}`; + const requestMsg = 'Creating action...'; + const successMsg = 'Created action successfully'; + const errorMsg = 'Creating action failed'; + const customOnSuccess = () => { + if (rawState.derive?.operation) { + persistDerivedAction(name, rawState.derive.operation); + } + dispatch(exportMetadata()) + .then(() => { + dispatch(createActionRequestComplete()); + if (redirect) { + dispatch( + push(`${globals.urlPrefix}${appPrefix}/manage/${name}/modify`) + ); + } + if (onSuccess) { + onSuccess(); + } + }) + .catch(e => { + dispatch(showErrorNotification('Action creation failed!', e?.message)); + }); + }; + const customOnError = () => { + dispatch(createActionRequestComplete()); + if (onFail) { + onFail(); + } + }; + dispatch(createActionRequestInProgress()); + makeMigrationCall( + dispatch, + getState, + migration.upMigration, + migration.downMigration, + migrationName, + customOnSuccess, + customOnError, + requestMsg, + successMsg, + errorMsg + ); +}; + export const createAction = (transformState, responseTransformState) => (dispatch, getState) => { const { add: rawState } = getState().actions; + const existingTypesList = customTypesSelector(getState()); const allActions = actionsSelector(getState()); - - const actionComment = rawState.comment ? rawState.comment.trim() : null; - - const { - name: actionName, - arguments: args, - outputType, - error: actionDefError, - type: actionType, - } = getActionDefinitionFromSdl(rawState.actionDefinition.sdl); - if (actionDefError) { - return dispatch( - showErrorNotification('Invalid Action Definition', actionDefError) - ); - } - - const { types, error: typeDefError } = getTypesFromSdl( - rawState.typeDefinition.sdl - ); - - if (typeDefError) { - return dispatch( - showErrorNotification('Invalid Types Definition', typeDefError) - ); - } - - const state = { - handler: rawState.handler.trim(), - kind: rawState.kind, - types, - actionType, - name: actionName, - arguments: args, - outputType, - headers: rawState.headers, - comment: actionComment, - timeout: parseInt(rawState.timeout, 10), - forwardClientHeaders: rawState.forwardClientHeaders, - }; - const requestTransform = getRequestTransformObject(transformState); const responseTransform = getResponseTransformObject( responseTransformState ); - const validationError = getStateValidationError(state, existingTypesList); - if (validationError) { - return dispatch(showErrorNotification(validationError)); - } - const typesWithRelationships = hydrateTypeRelationships( - state.types, - existingTypesList - ); - - const { types: mergedTypes, overlappingTypenames } = mergeCustomTypes( - typesWithRelationships, - existingTypesList - ); - - if (overlappingTypenames) { - const isOk = getOverlappingTypeConfirmation( - state.name, - allActions, + try { + const { name, migration } = createActionMigration( + rawState, existingTypesList, - overlappingTypenames + allActions, + requestTransform, + responseTransform ); - if (!isOk) { - return; - } + executeActionCreation(dispatch, getState, name, migration, rawState); + } catch (e) { + return dispatch(showErrorNotification(e.message, e.cause)); } - // Migration queries start - const migration = new Migration(); - - const customFieldsQueryUp = generateSetCustomTypesQuery( - reformCustomTypes(mergedTypes) - ); - - const customFieldsQueryDown = generateSetCustomTypesQuery( - reformCustomTypes(existingTypesList) - ); - migration.add(customFieldsQueryUp, customFieldsQueryDown); - const actionQueryUp = generateCreateActionQuery( - state.name, - generateActionDefinition(state, requestTransform, responseTransform), - actionComment - ); - - const actionQueryDown = generateDropActionQuery(state.name); - - migration.add(actionQueryUp, actionQueryDown); - // Migration queries end - - const migrationName = `create_action_${state.name}`; - const requestMsg = 'Creating action...'; - const successMsg = 'Created action successfully'; - const errorMsg = 'Creating action failed'; - const customOnSuccess = () => { - if (rawState.derive.operation) { - persistDerivedAction(state.name, rawState.derive.operation); - } - dispatch(exportMetadata()).then(() => { - dispatch(createActionRequestComplete()); - dispatch( - push(`${globals.urlPrefix}${appPrefix}/manage/${state.name}/modify`) - ); - }); - }; - const customOnError = () => { - dispatch(createActionRequestComplete()); - }; - dispatch(createActionRequestInProgress()); - makeMigrationCall( - dispatch, - getState, - migration.upMigration, - migration.downMigration, - migrationName, - customOnSuccess, - customOnError, - requestMsg, - successMsg, - errorMsg - ); }; export const saveAction = @@ -344,53 +388,63 @@ export const saveAction = ); }; -export const deleteAction = currentAction => (dispatch, getState) => { - const confirmMessage = `This will permanently delete the action "${currentAction.name}" from this table`; - const isOk = getConfirmation(confirmMessage, true, currentAction.name); - if (!isOk) { - return; - } +export const deleteAction = + currentAction => + (dispatch, getState, redirect = true, onSuccess, onFail) => { + const confirmMessage = `This will permanently delete the action "${currentAction.name}" from this table`; + const isOk = getConfirmation(confirmMessage, true, currentAction.name); + if (!isOk) { + return; + } - // Migration queries start - const migration = new Migration(); + // Migration queries start + const migration = new Migration(); - migration.add( - generateDropActionQuery(currentAction.name), - generateCreateActionQuery( - currentAction.name, - currentAction.definition, - currentAction.comment - ) - ); + migration.add( + generateDropActionQuery(currentAction.name), + generateCreateActionQuery( + currentAction.name, + currentAction.definition, + currentAction.comment + ) + ); - const migrationName = `delete_action_${currentAction.name}`; - const requestMsg = 'Deleting action...'; - const successMsg = 'Action deleted successfully'; - const errorMsg = 'Deleting action failed'; - const customOnSuccess = () => { - dispatch(modifyActionRequestComplete()); - dispatch(push(`${globals.urlPrefix}${appPrefix}/manage`)); - dispatch(exportMetadata()); - removePersistedDerivedAction(currentAction.name); + const migrationName = `delete_action_${currentAction.name}`; + const requestMsg = 'Deleting action...'; + const successMsg = 'Action deleted successfully'; + const errorMsg = 'Deleting action failed'; + const customOnSuccess = () => { + dispatch(modifyActionRequestComplete()); + if (redirect) { + dispatch(push(`${globals.urlPrefix}${appPrefix}/manage`)); + } + dispatch(exportMetadata()); + removePersistedDerivedAction(currentAction.name); + if (onSuccess) { + onSuccess(); + } + }; + const customOnError = () => { + dispatch(modifyActionRequestComplete()); + if (onFail) { + onFail(); + } + }; + + dispatch(modifyActionRequestInProgress()); + makeMigrationCall( + dispatch, + getState, + migration.upMigration, + migration.downMigration, + migrationName, + customOnSuccess, + customOnError, + requestMsg, + successMsg, + errorMsg + ); }; - const customOnError = () => { - dispatch(modifyActionRequestComplete()); - }; - - dispatch(modifyActionRequestInProgress()); - makeMigrationCall( - dispatch, - getState, - migration.upMigration, - migration.downMigration, - migrationName, - customOnSuccess, - customOnError, - requestMsg, - successMsg, - errorMsg - ); -}; export const addActionRel = (relConfig, successCb, existingRelConfig) => (dispatch, getState) => { diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js index 217a70d7fe6..3ce959c78b4 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js @@ -1,9 +1,13 @@ +import { DropdownButton } from '@/new-components/DropdownButton'; +import { Analytics } from '@/features/Analytics'; import React, { useMemo } from 'react'; -import { FaBook, FaEdit, FaWrench } from 'react-icons/fa'; -import { Link } from 'react-router'; +import { FaBook, FaEdit, FaFileImport, FaWrench } from 'react-icons/fa'; +import { browserHistory, Link } from 'react-router'; import LeftSubSidebar from '../../../Common/Layout/LeftSubSidebar/LeftSubSidebar'; import styles from '../../../Common/Layout/LeftSubSidebar/LeftSubSidebar.module.scss'; +import { isProConsole } from '@/utils'; +import { Badge } from '@/new-components/Badge'; const LeftSidebar = ({ appPrefix, @@ -17,13 +21,15 @@ const LeftSidebar = ({ const getSearchInput = () => { return ( - +
    + +
    ); }; @@ -101,6 +107,48 @@ const LeftSidebar = ({ addTrackId="action-tab-button-add-actions-sidebar" addTestString={'actions-sidebar-add-table'} childListTestString={'actions-table-links'} + addBtn={ + isProConsole(window.__env) ? ( +
    + +
    { + browserHistory.push(`${appPrefix}/manage/add`); + }} + > + New Action +
    + , + +
    { + browserHistory.push(`${appPrefix}/manage/add-oas`); + }} + > + {' '} + Import OpenAPI{' '} + + New + +
    +
    , + ], + ]} + > + Create +
    +
    + ) : undefined + } > {getChildList()} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.stories.tsx index dbf82047079..650f4468209 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.stories.tsx @@ -5,23 +5,25 @@ import { handlers } from '@/mocks/metadata.mock'; import { within, userEvent } from '@storybook/testing-library'; import { waitFor } from '@testing-library/react'; import { expect } from '@storybook/jest'; -import { OasGenerator, OasGeneratorProps } from './OASGenerator'; -import petstore from '../OASGeneratorModal/petstore.json'; +import { OASGenerator, OASGeneratorProps } from './OASGenerator'; +import petstore from './petstore.json'; export default { title: 'Features/Actions/OASGenerator', - component: OasGenerator, + component: OASGenerator, decorators: [ReactQueryDecorator()], parameters: { msw: handlers({ delay: 500 }), }, argTypes: { - onGenerate: { action: 'Generate Action' }, + onGenerate: { action: 'Create Action' }, + onDelete: { action: 'Create Action' }, + disabled: false, }, -} as Meta; +} as unknown as Meta; -export const Default: Story = args => { - return ; +export const Default: Story = args => { + return ; }; Default.play = async ({ canvasElement }) => { @@ -56,14 +58,4 @@ Default.play = async ({ canvasElement }) => { expect(canvas.queryAllByTestId(/^operation.*/)).toHaveLength(0); // clear search userEvent.clear(searchBox); - // Generate action button should be disabled - expect(canvas.getByText('Generate Action').parentElement).toBeDisabled(); - // click on the first operation - userEvent.click(canvas.getByTestId('operation-findPets')); - // wait for generate action button to be enabled - await waitFor(() => { - return expect( - canvas.getByText('Generate Action').parentElement - ).toHaveAttribute('disabled', ''); - }); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.tsx index 02a366390cf..7607d9f7bd7 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGenerator.tsx @@ -1,33 +1,24 @@ -import { SimpleForm } from '@/new-components/Form'; import React from 'react'; -import { z } from 'zod'; -import { GeneratedAction } from '../OASGeneratorModal'; +import { GeneratedAction } from './types'; +import { SimpleForm } from '@/new-components/Form'; import { OasGeneratorForm } from './OASGeneratorForm'; +import { formSchema } from './OASGeneratorPage'; -export interface OasGeneratorProps { - onGenerate: (values: GeneratedAction) => void; +export interface OASGeneratorProps { + onGenerate: (action: GeneratedAction) => void; + onDelete: (actionName: string) => void; + disabled: boolean; } -export const formSchema = z.object({ - oas: z.string(), - operation: z.string(), - url: z.string().url({ message: 'Invalid URL' }), - search: z.string(), -}); - -export const OasGenerator = (props: OasGeneratorProps) => { - const [values, setValues] = React.useState(); - +export const OASGenerator = (props: OASGeneratorProps) => { + const { onGenerate, onDelete, disabled } = props; return ( - { - if (values) { - props.onGenerate(values); - } - }} - > - + {}} schema={formSchema}> + ); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorActions.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorActions.tsx new file mode 100644 index 00000000000..75d11b5fc22 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorActions.tsx @@ -0,0 +1,96 @@ +import { Button } from '@/new-components/Button'; +import { Analytics } from '@/features/Analytics'; +import clsx from 'clsx'; +import React from 'react'; +import { FaChevronDown, FaExternalLinkAlt } from 'react-icons/fa'; +import { Operation } from './types'; + +export interface OasGeneratorActionsProps { + operation: Operation; + existing?: boolean; + onCreate: () => void; + onDelete: () => void; + disabled?: boolean; +} + +export const OasGeneratorActions: React.FC< + OasGeneratorActionsProps +> = props => { + const { operation, existing, onCreate, onDelete, disabled } = props; + const [isExpanded, setExpanded] = React.useState(false); + return ( +
    +
    +
    + {operation.path} +
    + {existing ? ( +
    + + + + + + +
    + ) : ( +
    +
    setExpanded(!isExpanded)} className="mr-5"> + More info + +
    + + + +
    + )} +
    +
    + {operation.description.trim() ?? + 'No description available for this endpoint'} +
    +
    + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorForm.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorForm.tsx index a04b88ca1e8..1dd06208126 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorForm.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorForm.tsx @@ -1,27 +1,24 @@ -import React, { useEffect } from 'react'; +import React, { ReactNode } from 'react'; import YAML from 'js-yaml'; -import { Button } from '@/new-components/Button'; import { CardedTable } from '@/new-components/CardedTable'; import { DropdownButton } from '@/new-components/DropdownButton'; import { CodeEditorField, InputField } from '@/new-components/Form'; -import { FaFilter, FaSearch } from 'react-icons/fa'; +import { FaExclamationTriangle, FaFilter, FaSearch } from 'react-icons/fa'; import { trackCustomEvent } from '@/features/Analytics'; import { useDebouncedEffect } from '@/hooks/useDebounceEffect'; import { Badge, BadgeColor } from '@/new-components/Badge'; import { Oas3 } from 'openapi-to-graphql'; -import { useIsUnmounted } from '@/components/Services/Data/DataSources/CreateDataSource/Neon/useIsUnmounted'; +import { useIsUnmounted } from '@/components/Services/Data/Common/tsUtils'; import { useFormContext } from 'react-hook-form'; -import { OasGeneratorMoreInfo } from './OASGeneratorMoreInfo'; -import { GeneratedAction, Operation } from '../OASGeneratorModal/types'; +import { useMetadata } from '@/features/MetadataAPI'; +import { hasuraToast } from '@/new-components/Toasts'; +import { OasGeneratorActions } from './OASGeneratorActions'; +import { GeneratedAction, Operation } from '../OASGenerator/types'; -import { generateAction, getOperations } from '../OASGeneratorModal/utils'; +import { generateAction, getOperations } from '../OASGenerator/utils'; import { UploadFile } from './UploadFile'; -export interface OasGeneratorFormProps { - setValues: (values?: GeneratedAction) => void; -} - -const fillToTenRows = (data: any[][]) => { +const fillToTenRows = (data: ReactNode[][]) => { const rowsToFill = 10 - data.length; for (let i = 0; i < rowsToFill; i++) { data.push([
    , '', '']); @@ -46,27 +43,29 @@ const editorOptions = { wrap: true, }; -export const OasGeneratorForm = (props: OasGeneratorFormProps) => { - const { setValues } = props; +interface OasGeneratorFormProps { + onGenerate: (action: GeneratedAction) => void; + onDelete: (actionName: string) => void; + disabled?: boolean; + saveOas?: (oas: string) => void; +} +export const OasGeneratorForm = (props: OasGeneratorFormProps) => { + const { onGenerate, onDelete, disabled } = props; const [operations, setOperations] = React.useState([]); const [parsedOas, setParsedOas] = React.useState(null); + const [isOasTooBig, setIsOasTooBig] = React.useState(false); + const [selectedMethods, setSelectedMethods] = React.useState([]); + const { data: metadata } = useMetadata(); + const isUnMounted = useIsUnmounted(); - const { - watch, - setValue, - setError, - clearErrors, - register, - trigger, - formState, - } = useFormContext(); + const { watch, setValue, setError, clearErrors, trigger, formState } = + useFormContext(); const oas = watch('oas'); - const operation = watch('operation'); const search = watch('search'); const url = watch('url'); @@ -85,7 +84,7 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => { }, [operations, search, selectedMethods]); const columns = operations?.length - ? [null, 'Method', 'Endpoint'] + ? ['Method', 'Endpoint'] : [ 2. All available endpoints will be listed here after the import @@ -93,41 +92,6 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => { , ]; - useEffect(() => { - (async () => { - if (parsedOas && operation) { - try { - const generatedAction = await generateAction(parsedOas, operation); - if (!isUnMounted()) { - await trigger('url'); - if (formState.isValid) { - setValues({ ...generatedAction, baseUrl: url }); - } else { - setValues(undefined); - } - } - } catch (e) { - setError('operation', { - message: `Failed to generate action: ${(e as Error).message}`, - }); - trackCustomEvent( - { - location: 'Import OAS Modal', - action: 'generate', - object: 'errors', - }, - { - data: { - errors: (e as Error).message, - }, - } - ); - console.error(e); - } - } - })(); - }, [setValues, operation, operations, url, parsedOas, setError, isUnMounted]); - useDebouncedEffect( async () => { let localParsedOas: Oas3 | undefined; @@ -136,6 +100,15 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => { if (oas && oas?.trim() !== '') { try { localParsedOas = JSON.parse(oas) as Oas3; + // if oas is smaller that 3mb + if (oas.length < 1024 * 1024 * 1) { + setIsOasTooBig(false); + if (props.saveOas) { + props.saveOas(oas); + } + } else { + setIsOasTooBig(true); + } } catch (e) { try { localParsedOas = YAML.load(oas) as Oas3; @@ -188,6 +161,55 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => { [oas, clearErrors, setError, setValue, url] ); + const createAction = async (operation: string) => { + if (parsedOas && operation) { + try { + const generatedAction = await generateAction(parsedOas, operation); + if (!isUnMounted()) { + await trigger('url'); + if (formState.isValid) { + onGenerate({ ...generatedAction, baseUrl: url }); + trackCustomEvent( + { + location: 'Import OAS Modal', + action: 'generate', + object: 'stats', + }, + { + data: { + size: JSON.stringify(oas?.length || 0), + numberOfOperations: JSON.stringify(operations?.length || 0), + }, + } + ); + } else { + hasuraToast({ + type: 'error', + title: 'Failed to generate action', + message: 'Please fill in all the required fields', + }); + } + } + } catch (e) { + setError('operation', { + message: `Failed to generate action: ${(e as Error).message}`, + }); + trackCustomEvent({ + location: 'Import OAS Modal', + action: 'generate', + object: 'error', + }); + // send notification + hasuraToast({ + type: 'error', + title: 'Failed to generate action', + message: (e as Error).message, + }); + console.error(e); + } + } + }; + const handleFileUpload = (e: React.ChangeEvent) => { const files = e.target.files; if (files) { @@ -225,13 +247,20 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => { noErrorPlaceholder /> (
    { if (selectedMethods.includes(method)) { setSelectedMethods( @@ -243,11 +272,13 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => { }} >
    - +
    + +
    {method.toUpperCase()}
    @@ -275,38 +306,44 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => {
    { - if (op.operationId === operation) { - return 'bg-gray-100'; - } - return ''; - })} className="h-[400px] relative" + showActionCell={false} columns={[...columns]} data={fillToTenRows( - filteredOperations.map(op => [ - , - - {op.method.toUpperCase()} - , -
    - -
    , - ]) + filteredOperations.map(op => { + const isActionAlreadyCreated = + metadata?.metadata?.actions?.some( + action => + action.name.toLowerCase() === op.operationId.toLowerCase() + ); + return [ + + {op.method.toUpperCase()} + , +
    + createAction(op.operationId)} + onDelete={() => onDelete(op.operationId)} + disabled={disabled} + /> +
    , + ]; + }) )} />
    + {isOasTooBig && ( +
    + The spec is + larger than 3MB. It won't be saved for future use. +
    + )}

    @@ -322,19 +359,6 @@ export const OasGeneratorForm = (props: OasGeneratorFormProps) => { />

    -
    - - -
    ); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorMoreInfo.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorMoreInfo.tsx deleted file mode 100644 index 6a08496a923..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorMoreInfo.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; -import { FaChevronDown } from 'react-icons/fa'; -import { Operation } from '../OASGeneratorModal/types'; - -export interface OasGeneratorMoreInfoProps { - operation: Operation; -} - -export const OasGeneratorMoreInfo: React.FC< - OasGeneratorMoreInfoProps -> = props => { - const { operation } = props; - const [isExpanded, setExpanded] = React.useState(false); - return ( -
    -
    setExpanded(!isExpanded)} - > -
    {operation.path}
    -
    - More info{' '} - -
    -
    -
    - {operation.description.trim() ?? - 'No description available for this endpoint'} -
    -
    - ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorPage.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorPage.tsx new file mode 100644 index 00000000000..efa2c9e641a --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorPage.tsx @@ -0,0 +1,148 @@ +import { + createActionMigration, + deleteAction, + executeActionCreation, +} from '@/components/Services/Actions/ServerIO'; +import { Link } from 'react-router'; +import { useMetadata } from '@/features/MetadataAPI'; +import { useDispatch, useStore } from 'react-redux'; +import { GeneratedAction } from './types'; +import { parseCustomTypes } from '../../../../shared/utils/hasuraCustomTypeUtils'; +import { generatedActionToHasuraAction } from '../OASGenerator/utils'; + +import { FaAngleRight, FaFileImport, FaHome } from 'react-icons/fa'; +import { z } from 'zod'; +import { useQueryClient } from 'react-query'; +import { isImportFromOpenAPIEnabled } from '@/utils'; +import { browserHistory } from 'react-router'; +import { SimpleForm } from '@/new-components/Form'; +import { OasGeneratorForm } from './OASGeneratorForm'; +import React from 'react'; +import { useLocalStorage } from '@/hooks'; + +export const formSchema = z.object({ + oas: z.string(), + url: z + .string() + .url({ message: 'Invalid URL' }) + .refine(val => !val.endsWith('/'), { + message: "Base URL can't end with a slash", + }), + search: z.string(), +}); + +export const Breadcrumbs = () => ( +
    + + + Actions + + +
    + + Import OpenAPI +
    +
    +); + +export const OASGeneratorPage = () => { + const dispatch = useDispatch(); + const store = useStore(); + const queryClient = useQueryClient(); + + const [savedOas, setSavedOas] = useLocalStorage('oas', ''); + + const { data: metadata } = useMetadata(); + const [busy, setBusy] = React.useState(false); + + const onGenerate = (action: GeneratedAction) => { + if (metadata) { + const { state, requestTransform, responseTransform } = + generatedActionToHasuraAction(action); + const actionMigration = createActionMigration( + state, + parseCustomTypes(metadata.metadata.custom_types ?? {}), + metadata.metadata.actions ?? [], + requestTransform, + responseTransform + ); + if (actionMigration) { + setBusy(true); + executeActionCreation( + dispatch, + store.getState, + actionMigration.name, + actionMigration.migration, + state, + false, + () => { + queryClient.invalidateQueries(['metadata']); + setBusy(false); + }, + () => { + setBusy(false); + } + ); + } + } + }; + + const onDelete = (actionName: string) => { + const action = metadata?.metadata?.actions?.find( + a => a.name === actionName + ); + if (action) { + setBusy(true); + deleteAction(action)( + dispatch, + store.getState, + false, + () => { + queryClient.invalidateQueries(['metadata']); + setBusy(false); + }, + () => { + setBusy(false); + } + ); + } + }; + + if (!isImportFromOpenAPIEnabled(window.__env)) { + browserHistory.push('/actions'); + return null; + } + + return ( +
    +
    +
    + +

    Import from OpenAPI spec

    +

    + Import a REST endpoint as an Action from an OpenAPI (OAS3) spec. +

    +
    +
    + {}} + schema={formSchema} + options={{ + defaultValues: { + oas: savedOas || '', + }, + }} + > + + +
    + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/external.types.d.ts b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/external.types.d.ts similarity index 100% rename from frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/external.types.d.ts rename to frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/external.types.d.ts diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/index.ts index 1c660b6e115..a31be1121a6 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/index.ts @@ -1 +1,2 @@ -export { OasGenerator } from './OASGenerator'; +export { OASGeneratorPage } from './OASGeneratorPage'; +export { GeneratedAction } from './types'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/petstore.json b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/petstore.json similarity index 100% rename from frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/petstore.json rename to frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/petstore.json diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/types.ts similarity index 94% rename from frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/types.ts rename to frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/types.ts index c721968e871..9add90f7e7e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/types.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/types.ts @@ -1,7 +1,7 @@ import { RequestTransformMethod } from '@/metadata/types'; import { createGraphQLSchema } from 'openapi-to-graphql'; import z from 'zod'; -import { formSchema } from './OASGeneratorForm'; +import { formSchema } from './OASGeneratorPage'; export type SchemaType = z.infer; export type GeneratedAction = { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/utils.test.ts b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/utils.test.ts similarity index 100% rename from frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/utils.test.ts rename to frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/utils.test.ts diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/utils.ts similarity index 86% rename from frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/utils.ts rename to frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/utils.ts index 958d5b7d29a..3267d15f216 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/utils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/utils.ts @@ -1,4 +1,8 @@ -import { RequestTransformMethod } from '@/metadata/types'; +import { + RequestTransform, + RequestTransformMethod, + ResponseTranform, +} from '@/metadata/types'; import { buildClientSchema, getIntrospectionQuery, @@ -485,3 +489,94 @@ export const getOperations = async (oas: Oas2 | Oas3): Promise => { const graphqlSchema = await parseOas(oas); return Object.values(graphqlSchema.data.operations); }; + +type ActionState = { + handler: string; + actionDefinition: { + sdl: string; + }; + typeDefinition: { + sdl: string; + }; + headers: { + name: string; + value: string; + type: 'static'; + }[]; + forwardClientHeaders: boolean; + kind: 'synchronous'; + timeout: string; + comment: string; +}; + +export const generatedActionToHasuraAction = ( + generatedAction: GeneratedAction +): { + state: ActionState; + requestTransform: RequestTransform; + responseTransform: ResponseTranform | null; +} => { + const state: ActionState = { + handler: generatedAction.baseUrl, + actionDefinition: { + sdl: generatedAction.action, + }, + typeDefinition: { + sdl: generatedAction.types, + }, + headers: generatedAction.headers.map(name => ({ + name, + value: `{{$body.input.${name}}}`, + type: 'static', + })), + forwardClientHeaders: true, + kind: 'synchronous', + timeout: '', + comment: generatedAction.description, + }; + + const requestTransform: RequestTransform = { + version: 2, + template_engine: 'Kriti', + method: generatedAction.method, + url: `{{$base_url}}${generatedAction.path}`, + query_params: + typeof generatedAction.queryParams === 'string' + ? generatedAction.queryParams + : generatedAction.queryParams.reduce( + (acc, curr) => ({ + ...acc, + [curr.name]: curr.value, + }), + + {} as Record + ), + + ...(generatedAction.requestTransforms + ? { + body: { + action: 'transform', + template: generatedAction.requestTransforms, + }, + } + : {}), + }; + + const responseTransform: ResponseTranform | null = + generatedAction.responseTransforms + ? { + version: 2, + body: { + action: 'transform', + template: generatedAction.responseTransforms, + }, + template_engine: 'Kriti', + } + : null; + + return { + state, + requestTransform, + responseTransform, + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorForm.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorForm.tsx deleted file mode 100644 index 2364d218baf..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorForm.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import React, { useEffect } from 'react'; -import z from 'zod'; -import { - CodeEditorField, - FieldWrapper, - InputField, - Radio, -} from '@/new-components/Form'; -import { FaSearch } from 'react-icons/fa'; -import { useFormContext } from 'react-hook-form'; -import { useDebouncedEffect } from '@/hooks/useDebounceEffect'; -import { Oas3 } from 'openapi-to-graphql'; -import YAML from 'js-yaml'; - -import { Badge, BadgeColor } from '@/new-components/Badge'; -import { trackCustomEvent } from '@/features/Analytics'; -import { useIsUnmounted } from '@/components/Services/Data/Common/tsUtils'; -import { generateAction, getOperations } from './utils'; -import { GeneratedAction, Operation } from './types'; - -export const formSchema = z.object({ - oas: z.string(), - operation: z.string(), - url: z.string().url({ message: 'Invalid URL' }), - search: z.string(), -}); - -const editorOptions = { - minLines: 16, - maxLines: 16, - showLineNumbers: true, - useSoftTabs: true, - showPrintMargin: false, -}; - -const badgeColors: Record = { - GET: 'green', - POST: 'blue', - PUT: 'yellow', - DELETE: 'red', - PATCH: 'purple', -}; - -export const OasGeneratorForm = (props: { - setValues: (values?: GeneratedAction) => void; -}) => { - const { setValues } = props; - - const [operations, setOperations] = React.useState([]); - const [parsedOas, setParsedOas] = React.useState(null); - const [isOasTooBig, setIsOasTooBig] = React.useState(false); - const [selectedMethods, setSelectedMethods] = React.useState([]); - - const isUnMounted = useIsUnmounted(); - - const { watch, setValue, setError, clearErrors, trigger, formState } = - useFormContext(); - const oas = watch('oas'); - const operation = watch('operation'); - const search = watch('search'); - const url = watch('url'); - - const filteredOperations = React.useMemo(() => { - return operations.filter(op => { - const searchMatch = - !search || - op.operationId.toLowerCase().includes(search.toLowerCase()) || - op.path.toLowerCase().includes(search.toLowerCase()) || - op.method.toLowerCase().includes(search.toLowerCase()); - const methodMatch = - selectedMethods.length === 0 || - selectedMethods.includes(op.method.toUpperCase()); - return searchMatch && methodMatch; - }); - }, [operations, search, selectedMethods]); - - useEffect(() => { - (async () => { - if (parsedOas && operation) { - try { - const generatedAction = await generateAction(parsedOas, operation); - await trigger('url'); - if (isUnMounted()) { - return; - } - - if (formState.isValid) { - setValues({ ...generatedAction, baseUrl: url }); - } else { - setValues(undefined); - } - } catch (e) { - setError('operation', { - message: `Failed to generate action: ${(e as Error).message}`, - }); - trackCustomEvent( - { - location: 'Import OAS Modal', - action: 'generate', - object: 'errors', - }, - { - data: { - errors: (e as Error).message, - }, - } - ); - console.error(e); - } - } - })(); - }, [setValues, operation, operations, url, parsedOas, setError, isUnMounted]); - - useDebouncedEffect( - async () => { - let localParsedOas: Oas3 | undefined; - clearErrors(); - if (oas && oas?.trim() !== '') { - try { - localParsedOas = JSON.parse(oas) as Oas3; - } catch (e) { - try { - localParsedOas = YAML.load(oas) as Oas3; - } catch (e2) { - setError('oas', { - message: 'Invalid JSON or YAML format', - }); - setOperations([]); - } - } - } - try { - if (localParsedOas) { - if (!url && localParsedOas.servers?.[0]?.url) { - setValue('url', localParsedOas.servers?.[0]?.url); - } else { - await trigger('url'); - if (isUnMounted()) { - return; - } - } - const ops = await getOperations(localParsedOas); - setOperations(ops); - - // send number of operations and file length - - const size = oas?.length || 0; - const numberOfOperations = ops?.length || 0; - - trackCustomEvent( - { - location: 'Import OAS Modal', - action: 'change', - object: 'specification', - }, - { - data: { - size, - numberOfOperations: numberOfOperations.toString(), - }, - } - ); - } - } catch (e) { - console.error(e); - setError('oas', { - message: `Invalid spec: ${(e as Error).message}`, - }); - } - - setParsedOas(localParsedOas ?? null); - }, - 400, - [oas, clearErrors, setError, setValue, url] - ); - - const handleFileUpload = (e: React.ChangeEvent) => { - const files = e.target.files; - if (files) { - const reader = new FileReader(); - reader.onload = loadEvent => { - if (loadEvent.target) { - // set isOasTooBig to true if the oas is larger than 512kb - setIsOasTooBig( - (loadEvent?.target?.result as string)?.length > 512 * 1024 - ); - setValue('oas', loadEvent.target.result); - } - }; - // set error - reader.onerror = () => { - setError('oas', { - message: 'Invalid spec', - }); - }; - - reader.readAsText(files[0]); - } - }; - - return ( -
    -
    -
    - -
    - -
    -
    - - {isOasTooBig ? ( -
    - File is too big to show -
    - ) : ( -
    - -
    - )} -
    - {operations?.length > 0 ? ( -
    - - - - - } - /> - {/* add badges to filter based on http method */} -
    - {Object.keys(badgeColors).map(method => ( - { - if (selectedMethods.includes(method)) { - setSelectedMethods( - selectedMethods.filter(m => m !== method) - ); - } else { - setSelectedMethods([...selectedMethods, method]); - } - }} - data-testid={`badge-${method}`} - key={method} - color={ - selectedMethods.includes(method) - ? badgeColors[method] - : 'gray' - } - className="cursor-pointer" - > - {method} - - ))} -
    -
    - - ({ - label: ( -
    - - {op.method.toUpperCase()} - - {op.path} -
    - ), - value: op.operationId, - })), - ]} - /> - {filteredOperations.length > 50 && ( -
    - {filteredOperations.length - 50} more endpoints... Use search to - filter them -
    - )} - {filteredOperations.length === 0 && ( -
    - No endpoints found. Try changing the search or the selected - method -
    - )} -
    - ) : null} -
    -
    - ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorModal.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorModal.stories.tsx deleted file mode 100644 index a795dc3b6a6..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorModal.stories.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { Story, Meta } from '@storybook/react'; -import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; -import { handlers } from '@/mocks/metadata.mock'; -import { within, userEvent } from '@storybook/testing-library'; -import { waitFor } from '@testing-library/react'; -import { expect } from '@storybook/jest'; -import { OasGeneratorModal, OasGeneratorModalProps } from './OASGeneratorModal'; -import petstore from './petstore.json'; - -export default { - title: 'Features/Actions/OASGeneratorModal', - component: OasGeneratorModal, - decorators: [ReactQueryDecorator()], - parameters: { - msw: handlers({ delay: 500 }), - }, -} as Meta; - -export const Default: Story = args => { - return ; -}; - -Default.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - const input = canvas.getByTestId('file'); - userEvent.upload( - input, - new File([JSON.stringify(petstore)], 'test.json', { - type: 'application/json', - }) - ); - - await waitFor(() => { - return canvas.queryByTestId('search'); - }); - - // wait for searchbox to appear - const searchBox = await canvas.findByTestId('search'); - // count number of operations - expect(canvas.getAllByTestId(/^operation.*/)).toHaveLength(4); - // search operations with 'get' - userEvent.type(searchBox, 'GET'); - // count filtered number of operations - expect(canvas.getAllByTestId(/^operation.*/)).toHaveLength(2); - // clear search - userEvent.clear(searchBox); - // search not existing operation - userEvent.type(searchBox, 'not-existing'); - // look for 'No endpoints found' message - expect(canvas.getByText(/No endpoints found/)).toBeInTheDocument(); - // clear search - userEvent.clear(searchBox); - // click on 'POST' badge - userEvent.click(canvas.getByTestId('badge-POST')); - // count filtered number of operations - expect(canvas.getAllByTestId(/^operation.*/)).toHaveLength(1); - // Generate action button should be disabled - expect(canvas.getByText('Generate Action').parentElement).toBeDisabled(); - // click on the first operation - userEvent.click(canvas.getByTestId(/^operation.*/)); - // wait for generate action button to be enabled - await waitFor(() => { - return expect( - canvas.getByText('Generate Action').parentElement - ).toHaveAttribute('disabled', ''); - }); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorModal.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorModal.tsx deleted file mode 100644 index b0154a4bc61..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/OASGeneratorModal.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { Dialog } from '@/new-components/Dialog'; -import { SimpleForm } from '@/new-components/Form'; - -import { formSchema, OasGeneratorForm } from './OASGeneratorForm'; -import { GeneratedAction } from './types'; - -export interface OasGeneratorModalProps { - onImport: (output: GeneratedAction) => void; - onClose: () => void; -} - -export const OasGeneratorModal = (props: OasGeneratorModalProps) => { - const { onClose, onImport } = props; - const [values, setValues] = React.useState(); - - return ( - { - onImport(values); - onClose(); - } - : undefined - } - onClose={onClose} - callToDeny="Cancel" - callToAction="Generate Action" - onSubmitAnalyticsName="action-tab-btn-generate-import-action-from-openapi" - onCancelAnalyticsName="action-tab-btn-cancel-import-action-from-openapi" - disabled={!values} - /> - } - title="Import OpenAPI endpoint" - hasBackdrop - onClose={onClose} - > -
    -

    - Generate your action from a Open API spec. -

    - {}} - > - - -
    -
    - ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/index.ts deleted file mode 100644 index 1cc1548f478..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGeneratorModal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { OasGeneratorModal } from './OASGeneratorModal'; -export { GeneratedAction } from './types'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/Actions/index.ts index 0b9da31ac5d..9eb8811282f 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/index.ts @@ -1,2 +1 @@ export * from './components/OASGenerator'; -export * from './components/OASGeneratorModal'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts b/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts index 070a1b9a221..449c4534133 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts @@ -11,17 +11,6 @@ export const availableFeatureFlagIds = { enabledNewUIForBigQuery, }; -const importActionFromOpenApi: FeatureFlagDefinition = { - id: importActionFromOpenApiId, - title: 'Import Action from OpenAPI', - description: - 'Try out the very experimental feature to generate one action from an OpenAPI endpoint', - section: 'data', - status: 'experimental', - defaultValue: false, - discussionUrl: '', -}; - export const availableFeatureFlags: FeatureFlagDefinition[] = [ { id: relationshipTabTablesId, @@ -42,6 +31,4 @@ export const availableFeatureFlags: FeatureFlagDefinition[] = [ defaultValue: false, discussionUrl: '', }, - // eslint-disable-next-line no-underscore-dangle - ...(isProConsole(window.__env) ? [importActionFromOpenApi] : []), ]; diff --git a/frontend/libs/console/legacy-ce/src/lib/hooks/index.ts b/frontend/libs/console/legacy-ce/src/lib/hooks/index.ts index 468181c3db1..fb115c94bf5 100644 --- a/frontend/libs/console/legacy-ce/src/lib/hooks/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/hooks/index.ts @@ -7,3 +7,4 @@ export * from './useFKRelationships'; export * from './usePrimaryKeys'; export * from './useCheckConstraints'; export * from './useUniqueKeys'; +export * from './useLocalStorage'; diff --git a/frontend/libs/console/legacy-ce/src/lib/hooks/useLocalStorage.ts b/frontend/libs/console/legacy-ce/src/lib/hooks/useLocalStorage.ts new file mode 100644 index 00000000000..07922ac8570 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/hooks/useLocalStorage.ts @@ -0,0 +1,31 @@ +import { useState } from 'react'; + +// Hook +export const useLocalStorage = ( + key: string, + initialValue: T +): [T, (value: T) => void] => { + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === 'undefined') { + return initialValue; + } + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.log(error); + return initialValue; + } + }); + const setValue = (value: T) => { + try { + setStoredValue(value); + if (typeof window !== 'undefined') { + window.localStorage.setItem(key, JSON.stringify(value)); + } + } catch (error) { + console.log(error); + } + }; + return [storedValue, setValue]; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/CardedTable/CardedTable.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/CardedTable/CardedTable.tsx index ed220f5b31a..7d40f6f821d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/CardedTable/CardedTable.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/CardedTable/CardedTable.tsx @@ -100,15 +100,14 @@ const TableBodyActionCell = ({ children }: ChildrenProps) => { interface BodyProps { data: ReactNode[][]; showActionCell?: boolean; - rowClassNames?: (string | undefined)[]; } -const Body = ({ data, showActionCell = false, rowClassNames }: BodyProps) => { +const Body = ({ data, showActionCell = false }: BodyProps) => { return ( {data.map((row, rowIndex) => { return ( - + {row.map((cell, index) => { if (showActionCell && index + 1 === row.length) { return {cell}; @@ -126,7 +125,6 @@ type CardedTableProps = HeaderProps & BodyProps & React.ComponentProps<'table'>; export const CardedTable = ({ columns, - rowClassNames, data, showActionCell, ...rest @@ -134,11 +132,7 @@ export const CardedTable = ({ return (
    - +
    ); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/DropdownButton/DropdownButton.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/DropdownButton/DropdownButton.tsx index 5b844b9fa54..ffcccaa3189 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/DropdownButton/DropdownButton.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/DropdownButton/DropdownButton.tsx @@ -1,17 +1,19 @@ import React from 'react'; import { FaChevronDown } from 'react-icons/fa'; import { Button } from '../Button'; -import { DropdownMenu } from '../DropdownMenu'; +import { DropdownMenu, DropdownMenuProps } from '../DropdownMenu'; interface DropdownButtonProps extends React.ComponentProps { items: React.ReactNode[][]; + options?: DropdownMenuProps['options']; } export const DropdownButton: React.FC = ({ items, + options, ...rest }) => ( - +