diff --git a/web-app-frontend/src/api/service.ts b/web-app-frontend/src/api/service.ts index 67b5ab0..0c62279 100644 --- a/web-app-frontend/src/api/service.ts +++ b/web-app-frontend/src/api/service.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Method, MockType } from '../const/common.const'; +import { MockType } from '../const/common.const'; const service = axios.create({ baseURL: '/web/app/mocks/rest/api', @@ -86,7 +86,7 @@ export interface MockMeta { export interface CreateMockRq { name: string; - method: Method; + method: string; path: string; type: MockType; delay: number, @@ -102,7 +102,7 @@ export interface GetMockRs { body: { serviceId: number; mockId: number; - method: Method; + method: string; name: string; path: string; type: MockType; @@ -117,7 +117,7 @@ export const getMock = (serviceId: number, mockId: number) => service.get; sortableColumns?: string[]; + sortByDefault?: { + column: string; + direction?: 'asc' | 'desc'; + }; filterableColumns?: string[]; styleProps?: StyleProps; onRowClick?: (row: Row) => void; @@ -49,6 +53,10 @@ const CustomTable: React.FC = ({ columns, data, sortableColumns = [], + sortByDefault = { + column: '', + direction: 'asc' + }, filterableColumns = [], styleProps = { centerHeaders: true, @@ -57,8 +65,8 @@ const CustomTable: React.FC = ({ onRowClick = null, }) => { const [filter, setFilter] = useState<{ [key: string]: string }>({}); - const [sortColumn, setSortColumn] = useState(''); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + const [sortColumn, setSortColumn] = useState(sortByDefault.column); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(sortByDefault.direction ?? 'asc'); // Handle filter changes const handleFilterChange = (event: React.ChangeEvent, columnKey: string) => { diff --git a/web-app-frontend/src/components/GraalVMMockContent.tsx b/web-app-frontend/src/components/GraalVMMockContent.tsx index 7a2d6f3..ce99788 100644 --- a/web-app-frontend/src/components/GraalVMMockContent.tsx +++ b/web-app-frontend/src/components/GraalVMMockContent.tsx @@ -71,7 +71,6 @@ const GraalVMMockContent: React.FC = ({ editorProps={{ $blockScrolling: true }} /> - ); }; diff --git a/web-app-frontend/src/components/suggestive-input/SuggestiveInput.css b/web-app-frontend/src/components/suggestive-input/SuggestiveInput.css new file mode 100644 index 0000000..3c089f6 --- /dev/null +++ b/web-app-frontend/src/components/suggestive-input/SuggestiveInput.css @@ -0,0 +1,23 @@ +.suggestions-dropdown { + position: absolute; + z-index: 1000; + background-color: white; + border-radius: 4px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + margin-top: 4px; +} + +.suggestion-item { + font-size: 0.875rem; + padding: 8px 12px; + cursor: pointer; +} + +.suggestion-text { + font-size: 0.75rem; + padding: 8px 12px; +} + +.suggestion-item:hover { + background-color: #f8f9fa; +} diff --git a/web-app-frontend/src/components/suggestive-input/SuggestiveInput.tsx b/web-app-frontend/src/components/suggestive-input/SuggestiveInput.tsx new file mode 100644 index 0000000..3e5ecbe --- /dev/null +++ b/web-app-frontend/src/components/suggestive-input/SuggestiveInput.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { FormControl, ListGroup } from 'react-bootstrap'; +import './SuggestiveInput.css'; + +export interface SuggestiveItem { + key: string; + value: string; + data?: any; +} + +export interface SuggestedItem { + key?: string; + value: string; + data?: any; +} + +interface SuggestiveInputProps { + id?: string; + type?: 'text' | 'number' + value?: string; + suggestions: SuggestiveItem[]; + maxSuggestions?: number; + mode: 'strict' | 'free'; + itemsToScroll?: number; + onFilter?: (input: string) => SuggestiveItem[]; + onChange: (value: SuggestedItem) => void; + placeholder?: string; + required: boolean; + disabled?: boolean; + clarifyText?: string; +} + +const SuggestiveInput: React.FC = ({ + id, + type = 'text', + value, + suggestions, + maxSuggestions = 5, + mode, + itemsToScroll = 5, + onFilter, + onChange, + placeholder, + required, + disabled, + clarifyText = 'Clarify request' + }) => { + const [inputValue, setInputValue] = useState(value ?? ''); + const [filteredSuggestions, setFilteredSuggestions] = useState( + suggestions.slice( + 0, + maxSuggestions + ) + ); + const [filteredSliced, setFilteredSliced] = useState(suggestions.length > filteredSuggestions.length); + const [showSuggestions, setShowSuggestions] = useState(false); + const inputRef = useRef(null); + const [dropdownWidth, setDropdownWidth] = useState(undefined); + + if (!onFilter) { + onFilter = it => { + const substring = it.toLowerCase(); + return suggestions.filter(it => it.value.includes(substring)); + }; + } + + useEffect(() => { + if (inputRef.current) { + setDropdownWidth(inputRef.current.offsetWidth); + } + }, [inputValue]); + + const handleValueChange = (value: string): SuggestiveItem[] => { + setInputValue(value); + + const filtered = onFilter(value); + const sliced = filtered.slice(0, maxSuggestions); + setFilteredSuggestions(sliced); + setFilteredSliced(filtered.length > sliced.length); + return sliced; + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const sliced = handleValueChange(value); + + setShowSuggestions(sliced.length > 0); + + if (sliced.length > 0) { + const candidate = sliced[0]; + onChange({ + key: candidate.key, + value: candidate.value, + data: candidate.data + }); + } else if (mode === 'free') { + onChange({ value: value }); + } else { + onChange({ value: '' }); + } + }; + + const handleSuggestionClick = (suggestion: SuggestiveItem) => { + handleValueChange(suggestion.key); + setShowSuggestions(false); + onChange(suggestion); + }; + + const handleBlur = () => { + setTimeout(() => setShowSuggestions(false), 200); + }; + + return ( + <> + setShowSuggestions(filteredSuggestions.length > 0)} + onBlur={handleBlur} + placeholder={placeholder} + required={required} + disabled={disabled} + /> + {showSuggestions && ( +
+ + {filteredSuggestions.map((suggestion) => ( + handleSuggestionClick(suggestion)} + className="suggestion-item" + > + {suggestion.value} + + ))} + {filteredSliced && ( + + {clarifyText} + + )} + +
+ )} + + ); +}; + +export default SuggestiveInput; diff --git a/web-app-frontend/src/const/common.const.ts b/web-app-frontend/src/const/common.const.ts index 02c7bbd..3dbeacb 100644 --- a/web-app-frontend/src/const/common.const.ts +++ b/web-app-frontend/src/const/common.const.ts @@ -9,8 +9,6 @@ export const methods = [ 'DELETE' ] as const; -export type Method = typeof methods[number]; - export const statusCodes = new Map([ [100, 'Continue'] as const, [101, 'Switching Protocols'] as const, @@ -77,8 +75,6 @@ export const statusCodes = new Map([ export type MapKey> = T extends Map ? K : never -export type StatusCode = MapKey - export const mimeToAceModeMap = new Map([ ['text/html', 'html'] as const, ['text/css', 'css'] as const, diff --git a/web-app-frontend/src/pages/invocations/MockInvocationListPage.tsx b/web-app-frontend/src/pages/invocations/MockInvocationListPage.tsx index 63f95c5..bd02509 100644 --- a/web-app-frontend/src/pages/invocations/MockInvocationListPage.tsx +++ b/web-app-frontend/src/pages/invocations/MockInvocationListPage.tsx @@ -139,6 +139,7 @@ const MockInvocationListPage: React.FC = () => { })} onRowClick={handleRowClick} sortableColumns={['method', 'path', 'timing', 'status', 'createdAt']} + sortByDefault={{column: 'createdAt', direction: 'desc'}} filterableColumns={['method', 'path', 'timing', 'status', 'createdAt']} styleProps={{ centerHeaders: true, diff --git a/web-app-frontend/src/pages/invocations/invocation/MockInvocationPage.tsx b/web-app-frontend/src/pages/invocations/invocation/MockInvocationPage.tsx index 1b3cfbc..1a7603b 100644 --- a/web-app-frontend/src/pages/invocations/invocation/MockInvocationPage.tsx +++ b/web-app-frontend/src/pages/invocations/invocation/MockInvocationPage.tsx @@ -115,7 +115,7 @@ const MockInvocationPage: React.FC = () => { {!error && ( - +
@@ -149,7 +149,7 @@ const MockInvocationPage: React.FC = () => {
Remote Client Host

Query Params

- +
@@ -162,7 +162,7 @@ const MockInvocationPage: React.FC = () => {
Key

Request Headers

- +
@@ -182,7 +182,7 @@ const MockInvocationPage: React.FC = () => { />

Response Headers

-
Key
+
diff --git a/web-app-frontend/src/pages/mock/AddEditMockPage.tsx b/web-app-frontend/src/pages/mock/AddEditMockPage.tsx index e9e8726..f648401 100644 --- a/web-app-frontend/src/pages/mock/AddEditMockPage.tsx +++ b/web-app-frontend/src/pages/mock/AddEditMockPage.tsx @@ -1,22 +1,36 @@ import React, { useEffect, useState } from 'react'; -import { createMock, getMock, updateMock } from '../../api/service'; +import { createMock, getMock, MockMeta, updateMock } from '../../api/service'; import { useNavigate, useParams } from 'react-router-dom'; -import { contextPath, Method, methods, MockType } from '../../const/common.const'; +import { contextPath, methods, MockType } from '../../const/common.const'; import { decodeToBuffer, encode } from '../../utils/base64'; import MockForm from './MockForm'; +export interface ModifyingMock { + name: string; + method: string; + path: string; + type: MockType; + delay: number; + meta: MockMeta; + content: ArrayBuffer; +} + const AddEditMockPage: React.FC = () => { const [loading, setLoading] = useState(true); const navigate = useNavigate(); const { serviceId, mockId } = useParams(); - const [mockName, setMockName] = useState(''); - const [method, setMethod] = useState(methods[0]); - const [path, setPath] = useState(''); - const [delay, setDelay] = useState(0); - const [mockType, setMockType] = useState('STATIC'); - const [meta, setMeta] = useState<{ [key: string]: string }>({ STATUS_CODE: '200' }); - const [content, setContent] = useState(new ArrayBuffer(0)); + const [modifyingMock, setModifyingMock] = useState({ + name: '', + method: methods[0], + path: '', + type: 'STATIC', + delay: 0, + meta: { + STATUS_CODE: '200' + }, + content: new ArrayBuffer(0) + }); useEffect(() => { if (mockId) { @@ -35,13 +49,15 @@ const AddEditMockPage: React.FC = () => { try { const response = await getMock(+serviceId, +mockId); const body = response.data.body; - setMockName(body.name); - setMethod(body.method); - setPath(body.path.slice(1)); - setDelay(body.delay); - setMockType(body.type); - setMeta(body.meta); - setContent(decodeToBuffer(body.content)); + setModifyingMock({ + name: body.name, + method: body.method, + path: body.path.slice(1), + delay: body.delay, + type: body.type, + meta: body.meta, + content: decodeToBuffer(body.content) + }) } catch (error) { console.error('Failed to fetch mock:', error); } finally { @@ -58,13 +74,13 @@ const AddEditMockPage: React.FC = () => { e.preventDefault(); try { const mockData = { - name: mockName, - method, - path: '/' + path, - type: mockType, - delay: delay, - meta, - content: encode(content) + name: modifyingMock.name, + method: modifyingMock.method, + path: '/' + modifyingMock.path, + type: modifyingMock.type, + delay: modifyingMock.delay, + meta: modifyingMock.meta, + content: encode(modifyingMock.content) }; if (mockId) { await updateMock(+serviceId, +mockId, mockData); @@ -84,20 +100,8 @@ const AddEditMockPage: React.FC = () => { return ( void; - setMethod: (method: Method) => void; - setPath: (path: string) => void; - setDelay: (delay: number) => void; - setMockType: (type: MockType) => void; - setMeta: (meta: { [key: string]: string }) => void; - setContent: (content: ArrayBuffer) => void; + modifyingMock: ModifyingMock; + setModifyingMock: (value: ModifyingMock) => void; onSubmit: (e: React.FormEvent) => void; isEditMode: boolean; navigateBack: () => void; @@ -32,34 +23,15 @@ type MockFormProps = { export const MockForm: React.FC = ({ loading, - mockName, - method, - path, - delay, - mockType, - meta, - content, - setMockName, - setMethod, - setPath, - setDelay, - setMockType, - setMeta, - setContent, + modifyingMock, + setModifyingMock, onSubmit, isEditMode, navigateBack }) => { - const onStatusChange = (e: React.ChangeEvent) => { - const newStatusCode = +e.target.value as StatusCode; - if (newStatusCode) { - setMeta({ ...meta, STATUS_CODE: `${newStatusCode}` }); - } - }; - const onMockTypeChange = (e: React.ChangeEvent) => { - setMockType(e.target.value as MockType); + setModifyingMock({...modifyingMock, type: e.target.value as MockType}) }; const pathTooltip = ( @@ -117,35 +89,43 @@ export const MockForm: React.FC = ({
{/* Mock Name Input */} - Name - setMockName(e.target.value)} - required - /> + +
+ Name + + + setModifyingMock({...modifyingMock, name: e.target.value})} + required + /> + + {/* Method and Path Fields */} - + HTTP Method - setMethod(methods[e.target.selectedIndex])} - required - > - {methods.map((it) => ( - - ))} - + setModifyingMock({...modifyingMock, method: it.value})} + required={true} + suggestions={methods.map(it => { + return { key: it, value: it }; + })} + maxSuggestions={methods.length} + /> - + - Path + Path / = ({ > setPath(e.target.value)} + id={'pathInput'} + value={modifyingMock.path} + onChange={e => setModifyingMock({...modifyingMock, path: e.target.value})} placeholder="Ant pattern or path" required /> @@ -169,83 +150,113 @@ export const MockForm: React.FC = ({ {/* HTTP Headers */} Http Headers - + setModifyingMock({...modifyingMock, meta: it})} + /> {/* Status and Mock Type Fields */} - + - Status - - {Array.from(statusCodes.keys()).map((it) => ( - - ))} - + + + Status + + + setModifyingMock({...modifyingMock, meta: { + ...modifyingMock.meta, + STATUS_CODE: `${it.key ?? it.value}` + }})} + required={true} + suggestions={Array.from(statusCodes).map(([key, value]) => { + return { key: `${key}`, value: `${key}: ${value}` }; + })} + maxSuggestions={10} + /> + + - - Delay (ms) - setDelay(+e.target.value)} - required - /> + + + + Delay + + + + setModifyingMock({...modifyingMock, delay: +e.target.value})} + required + /> + ms + + + - - - Type - - { - Array.from(mockTypes.keys()).map( - it => ( - - ) - ) - } - + + + + + Type + + + + { + Array.from(mockTypes).map( + ([key, value]) => ( + + ) + ) + } + + + {/* Content Section Based on Mock Type */} - {mockType === 'STATIC' && ( + {modifyingMock.type === 'STATIC' && ( setModifyingMock({...modifyingMock, content: it})} + meta={modifyingMock.meta} + setMeta={it => setModifyingMock({...modifyingMock, meta: it})} creation={!isEditMode} /> )} - {mockType === 'STATIC_FILE' && setModifyingMock({...modifyingMock, content: it})} isEditMode={isEditMode} />} - {(mockType === 'JS' || mockType === 'PYTHON') && ( + {(modifyingMock.type === 'JS' || modifyingMock.type === 'PYTHON') && ( setModifyingMock({...modifyingMock, content: it})} /> )} {/* Submit Button */} - + + + {(modifyingMock.type === 'JS' || modifyingMock.type === 'PYTHON') && ( + + )} diff --git a/web-app-frontend/src/pages/mocks/ServiceMocksListPage.tsx b/web-app-frontend/src/pages/mocks/ServiceMocksListPage.tsx index d97d3e2..7261431 100644 --- a/web-app-frontend/src/pages/mocks/ServiceMocksListPage.tsx +++ b/web-app-frontend/src/pages/mocks/ServiceMocksListPage.tsx @@ -115,6 +115,7 @@ const ServiceMocksListPage: React.FC = () => { ]} data={mocks.map(mock => { return { + mockId: mock.mockId, method: { representation: {mock.method}, value: mock.method @@ -145,6 +146,7 @@ const ServiceMocksListPage: React.FC = () => { }; })} sortableColumns={['method', 'name', 'path', 'type']} + sortByDefault={{column: 'mockId'}} filterableColumns={['method', 'name', 'path', 'type']} styleProps={{ centerHeaders: true, diff --git a/web-app-frontend/src/pages/service/ServiceListPage.tsx b/web-app-frontend/src/pages/service/ServiceListPage.tsx index 5530772..4564748 100644 --- a/web-app-frontend/src/pages/service/ServiceListPage.tsx +++ b/web-app-frontend/src/pages/service/ServiceListPage.tsx @@ -201,6 +201,7 @@ const ServiceListPage: React.FC = () => { }; })} sortableColumns={['code']} + sortByDefault={{column: 'serviceId'}} filterableColumns={['code']} styleProps={{ centerHeaders: true, diff --git a/web-app-frontend/src/pages/service/ServiceModal.tsx b/web-app-frontend/src/pages/service/ServiceModal.tsx index bd649c8..b065d9d 100644 --- a/web-app-frontend/src/pages/service/ServiceModal.tsx +++ b/web-app-frontend/src/pages/service/ServiceModal.tsx @@ -25,7 +25,7 @@ export const ServiceModal: React.FC = ({ {modalMode === 'edit' ? 'Edit Service' : 'Add New Service'} -
+ {handleSave(); return false;}}> Service Code { ]} data={mocks.map(mock => { return { + mockId: mock.mockId, export: { representation: { }; })} sortableColumns={['method', 'name', 'path', 'type']} + sortByDefault={{column: 'mockId'}} filterableColumns={['method', 'name', 'path', 'type']} styleProps={{ centerHeaders: true, diff --git a/web-app-frontend/src/pages/share/ServiceMocksListImportPage.tsx b/web-app-frontend/src/pages/share/ServiceMocksListImportPage.tsx index ef4c621..7dfdfed 100644 --- a/web-app-frontend/src/pages/share/ServiceMocksListImportPage.tsx +++ b/web-app-frontend/src/pages/share/ServiceMocksListImportPage.tsx @@ -166,6 +166,7 @@ const ServiceMocksListImportPage: React.FC = () => { { key: 'enabled', label: 'Enabled' }, ]} data={service.mocks.map((mock, mockIndex) => ({ + mockIndex: mockIndex, import: { representation: ( { }, }))} sortableColumns={['method', 'name', 'path', 'type']} + sortByDefault={{column: 'mockIndex'}} filterableColumns={['method', 'name', 'path', 'type']} styleProps={{ centerHeaders: true, textCenterValues: true }} />
Key