From 7f6b67a01331a24aa66f510ca345670b452dee60 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 27 Nov 2024 14:22:42 +0300 Subject: [PATCH 1/7] Add search in HistoryV9 component --- src/app/views/sidebar/SidebarV9.tsx | 4 +- src/app/views/sidebar/history/HistoryV9.tsx | 49 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/app/views/sidebar/history/HistoryV9.tsx diff --git a/src/app/views/sidebar/SidebarV9.tsx b/src/app/views/sidebar/SidebarV9.tsx index d031a7676..a67e41b19 100644 --- a/src/app/views/sidebar/SidebarV9.tsx +++ b/src/app/views/sidebar/SidebarV9.tsx @@ -11,6 +11,7 @@ import { IDimensions } from '../../../types/dimensions'; import { setDimensions } from '../../services/slices/dimensions.slice'; import { translateMessage } from '../../utils/translate-messages'; import { History } from './history'; +import { HistoryV9 } from './history/HistoryV9'; import ResourceExplorer from './resource-explorer'; import { SampleQueriesV9 } from './sample-queries/SampleQueriesV9'; @@ -62,7 +63,7 @@ const SidebarV9 = ()=>{ const tabItems: Record = { 'sample-queries': , 'resources': , - 'history': + 'history': } // TODO: Resizing is not showing/ hiding sidebar. Should be checked when // updated to v9 @@ -119,3 +120,4 @@ const getDimensions = (show: boolean, dimensions: IDimensions)=>{ } export { SidebarV9 }; + diff --git a/src/app/views/sidebar/history/HistoryV9.tsx b/src/app/views/sidebar/history/HistoryV9.tsx new file mode 100644 index 000000000..857a1b98c --- /dev/null +++ b/src/app/views/sidebar/history/HistoryV9.tsx @@ -0,0 +1,49 @@ +import { InputOnChangeData, makeStyles, SearchBox, SearchBoxChangeEvent } from '@fluentui/react-components' +import { useEffect, useRef, useState } from 'react' +import { useAppSelector } from '../../../../store' +import { IHistoryItem } from '../../../../types/history' +import { translateMessage } from '../../../utils/translate-messages' + +const useStyles = makeStyles({ + searchBox: { + width: '100%', + maxWidth: '100%' + } +}) + +export const HistoryV9 = ()=>{ + const styles = useStyles() + const shouldGenerateGroups = useRef(true) + const history = useAppSelector(state=> state.history) + const [historyItems, setHistoryItems] = useState(history) + const [searchStarted, setSearchStarted] = useState(false); + + useEffect(()=>{ + setHistoryItems(history) + }, [history]) + + const handleSearchValueChanged = (_: SearchBoxChangeEvent, data: InputOnChangeData)=>{ + shouldGenerateGroups.current = true + setSearchStarted(true) + let content = [...history] + const searchValue = data.value.trim() ?? ''; + if(searchValue) { + content = history.filter((item:IHistoryItem)=>{ + const name = item.url.toLowerCase() + return name.includes(searchValue) + }) + } + setHistoryItems(content) + console.log(historyItems) + } + return <> + + +
+
+ +} \ No newline at end of file From 10250aebe708dafa1f0a7475475c66d4456c1f29 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 27 Nov 2024 14:28:57 +0300 Subject: [PATCH 2/7] feat: enhance HistoryV9 component with message bar and improved layout --- src/app/views/sidebar/history/HistoryV9.tsx | 25 ++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/views/sidebar/history/HistoryV9.tsx b/src/app/views/sidebar/history/HistoryV9.tsx index 857a1b98c..bba842395 100644 --- a/src/app/views/sidebar/history/HistoryV9.tsx +++ b/src/app/views/sidebar/history/HistoryV9.tsx @@ -1,10 +1,23 @@ -import { InputOnChangeData, makeStyles, SearchBox, SearchBoxChangeEvent } from '@fluentui/react-components' +import { + Divider, + InputOnChangeData, + makeStyles, + MessageBar, + MessageBarBody, + SearchBox, + SearchBoxChangeEvent +} from '@fluentui/react-components' import { useEffect, useRef, useState } from 'react' import { useAppSelector } from '../../../../store' import { IHistoryItem } from '../../../../types/history' import { translateMessage } from '../../../utils/translate-messages' const useStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + gap: '4px' + }, searchBox: { width: '100%', maxWidth: '100%' @@ -36,14 +49,20 @@ export const HistoryV9 = ()=>{ setHistoryItems(content) console.log(historyItems) } - return <> + return
+ + + + {translateMessage('Your history includes queries made in the last 30 days')} + +
- +
} \ No newline at end of file From b780823e03afd90a7c1c005465fbb071b782dd9c Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 27 Nov 2024 15:08:11 +0300 Subject: [PATCH 3/7] feat: refactor dynamicSort function to use generics and update related components --- src/app/utils/dynamic-sort.spec.ts | 34 ++---- src/app/utils/dynamic-sort.ts | 6 +- .../request/permissions/Permissions.Full.tsx | 2 +- src/app/views/sidebar/history/History.tsx | 2 +- src/app/views/sidebar/history/HistoryV9.tsx | 105 ++++++++++++++++-- 5 files changed, 106 insertions(+), 43 deletions(-) diff --git a/src/app/utils/dynamic-sort.spec.ts b/src/app/utils/dynamic-sort.spec.ts index 630a49f78..7572e9065 100644 --- a/src/app/utils/dynamic-sort.spec.ts +++ b/src/app/utils/dynamic-sort.spec.ts @@ -1,6 +1,9 @@ -import { dynamicSort } from './dynamic-sort'; import { SortOrder } from '../../types/enums'; - +import { dynamicSort } from './dynamic-sort'; +interface INameAge { + name: string + age: number +} describe('Dynamic Sort', () => { it('should sort an array of objects in ascending order', () => { @@ -16,7 +19,7 @@ describe('Dynamic Sort', () => { { name: 'Lucy', age: 18 } ]; - const sortedArray = arrayToSort.sort(dynamicSort('name', SortOrder.ASC)); + const sortedArray = arrayToSort.sort(dynamicSort('name', SortOrder.ASC)); expect(expected).toEqual(sortedArray); }); @@ -33,30 +36,7 @@ describe('Dynamic Sort', () => { { name: 'Ann', age: 14 } ]; - const sortedArray = arrayToSort.sort(dynamicSort('name', SortOrder.DESC)); + const sortedArray = arrayToSort.sort(dynamicSort('name', SortOrder.DESC)); expect(expected).toEqual(sortedArray); }); - - it('should return unsorted object array when property does not exist', () => { - const arrayToSort = [ - { name: 'Ann', age: 14 }, - { name: 'Lucy', age: 18 }, - { name: 'Diana', age: 11 } - ]; - - const sortedArray = arrayToSort.sort(dynamicSort('gender', SortOrder.DESC)); - expect(arrayToSort).toEqual(sortedArray); - }); - - it('should return unsorted object array when property is empty', () => { - const arrayToSort = [ - { name: 'Ann', age: 14 }, - { name: 'Lucy', age: 18 }, - { name: 'Diana', age: 11 } - ]; - - const sortedArray = arrayToSort.sort(dynamicSort('', SortOrder.DESC)); - expect(arrayToSort).toEqual(sortedArray); - }); - }); diff --git a/src/app/utils/dynamic-sort.ts b/src/app/utils/dynamic-sort.ts index a66cacae5..299441d29 100644 --- a/src/app/utils/dynamic-sort.ts +++ b/src/app/utils/dynamic-sort.ts @@ -5,18 +5,18 @@ import { SortOrder } from '../../types/enums'; * @param {SortOrder} sortOrder the direction to follow Ascending / Descending * You pass this helper to the array sort function */ -export function dynamicSort(property: string | null, sortOrder: SortOrder) { +export function dynamicSort(property: keyof T, sortOrder: SortOrder) { let order = 1; if (sortOrder === SortOrder.DESC) { order = -1; } if (property) { - return (first: any, second: any) => { + return (first: T, second: T) => { const result = (first[property] < second[property]) ? -1 : (first[property] > second[property]) ? 1 : 0; return result * order; }; } - return (first: any, second: any) => { + return (first: T, second: T) => { const result = (first < second) ? -1 : (first > second) ? 1 : 0; return result * order; }; diff --git a/src/app/views/query-runner/request/permissions/Permissions.Full.tsx b/src/app/views/query-runner/request/permissions/Permissions.Full.tsx index 3a2d6204c..6c0c85ca3 100644 --- a/src/app/views/query-runner/request/permissions/Permissions.Full.tsx +++ b/src/app/views/query-runner/request/permissions/Permissions.Full.tsx @@ -59,7 +59,7 @@ const FullPermissions: React.FC> = (): JSX.Element => { const sortPermissions = (permissionsToSort: IPermission[]): IPermission[] => { try { - return [...permissionsToSort].sort(dynamicSort('value', SortOrder.ASC)); + return [...permissionsToSort].sort(dynamicSort('value', SortOrder.ASC)); } catch (error) { // ignore } diff --git a/src/app/views/sidebar/history/History.tsx b/src/app/views/sidebar/history/History.tsx index a89b03e76..bf7fe765d 100644 --- a/src/app/views/sidebar/history/History.tsx +++ b/src/app/views/sidebar/history/History.tsx @@ -57,7 +57,7 @@ const yesterday = formatDate(yesterdaysDate); yesterdaysDate.setDate(yesterdaysDate.getDate() - 1); const sortItems = (content: IHistoryItem[]) => { - content.sort(dynamicSort('createdAt', SortOrder.DESC)); + content.sort(dynamicSort('createdAt', SortOrder.DESC)); content.forEach((value: any, index: number) => { value.index = index; }); diff --git a/src/app/views/sidebar/history/HistoryV9.tsx b/src/app/views/sidebar/history/HistoryV9.tsx index bba842395..7f1becfcb 100644 --- a/src/app/views/sidebar/history/HistoryV9.tsx +++ b/src/app/views/sidebar/history/HistoryV9.tsx @@ -1,16 +1,39 @@ import { + AriaLiveAnnouncer, Divider, InputOnChangeData, + Label, makeStyles, MessageBar, MessageBarBody, SearchBox, - SearchBoxChangeEvent -} from '@fluentui/react-components' -import { useEffect, useRef, useState } from 'react' -import { useAppSelector } from '../../../../store' -import { IHistoryItem } from '../../../../types/history' -import { translateMessage } from '../../../utils/translate-messages' + SearchBoxChangeEvent, + Text +} from '@fluentui/react-components'; +import { IGroup } from '@fluentui/react/lib/DetailsList'; + +import { useEffect, useRef, useState } from 'react'; +import { useAppSelector } from '../../../../store'; +import { SortOrder } from '../../../../types/enums'; +import { IHistoryItem } from '../../../../types/history'; +import { dynamicSort } from '../../../utils/dynamic-sort'; +import { generateGroupsFromList } from '../../../utils/generate-groups'; +import { translateMessage } from '../../../utils/translate-messages'; + +const formatDate = (date: Date) => { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const monthStr = (month < 10 ? '0' : '') + month; + const day = date.getDate(); + const dayStr = (day < 10 ? '0' : '') + day; + return `${year}-${monthStr}-${dayStr}`; +}; + + +const today = formatDate(new Date()); +const yesterdaysDate = new Date(); +const yesterday = formatDate(yesterdaysDate); +yesterdaysDate.setDate(yesterdaysDate.getDate() - 1); const useStyles = makeStyles({ container: { @@ -24,12 +47,69 @@ const useStyles = makeStyles({ } }) +interface HistoryProps { + history: IHistoryItem[] + groups: IGroup[] + searchValue: string +} + +const History = (props: HistoryProps)=>{ + console.log(props) + return

Items

+} + +const sortItems = (content: IHistoryItem[]) => { + content.sort(dynamicSort('createdAt', SortOrder.DESC)); + content.forEach((value: IHistoryItem, index: number) => { + value.index = index; + }); + return content; +} + +const getCategory = (historyItem: IHistoryItem) => { + const olderText = translateMessage('older'); + const todayText = translateMessage('today'); + const yesterdayText = translateMessage('yesterday'); + let category = olderText; + if (historyItem.createdAt.includes(today)) { + category = todayText; + } else if (historyItem.createdAt.includes(yesterday)) { + category = yesterdayText; + } + return category; +} + +const getItems = (content: IHistoryItem[]): IHistoryItem[] => { + const list: IHistoryItem[] = []; + content.forEach((historyItem) => { + list.push({ + ...historyItem, + category: getCategory(historyItem) + }); + }); + return sortItems(list); +} + + export const HistoryV9 = ()=>{ const styles = useStyles() - const shouldGenerateGroups = useRef(true) const history = useAppSelector(state=> state.history) const [historyItems, setHistoryItems] = useState(history) + const [searchValue, setSearchValue] = useState(''); const [searchStarted, setSearchStarted] = useState(false); + const shouldGenerateGroups = useRef(true) + const [groups, setGroups] = useState([]); + + const items = getItems(historyItems); + + useEffect(() => { + if (shouldGenerateGroups.current) { + setGroups(generateGroupsFromList(items, 'category')); + if (groups && groups.length > 0) { + shouldGenerateGroups.current = false; + } + } + }, [historyItems, searchStarted, shouldGenerateGroups]) useEffect(()=>{ setHistoryItems(history) @@ -39,15 +119,15 @@ export const HistoryV9 = ()=>{ shouldGenerateGroups.current = true setSearchStarted(true) let content = [...history] - const searchValue = data.value.trim() ?? ''; - if(searchValue) { + const value = data.value.trim() ?? ''; + if(value) { + setSearchValue(value) content = history.filter((item:IHistoryItem)=>{ const name = item.url.toLowerCase() - return name.includes(searchValue) + return name.includes(value) }) } setHistoryItems(content) - console.log(historyItems) } return
{ {translateMessage('Your history includes queries made in the last 30 days')} + {historyItems.length === 0 && } + {`${historyItems.length} search results available.`} +
From 116e8975f727d3cda8e764a1bf41b064b47db2e8 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 27 Nov 2024 16:17:31 +0300 Subject: [PATCH 4/7] feat: implement FlatTree for history display with download icon and badge count --- src/app/views/sidebar/history/HistoryV9.tsx | 74 +++++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/src/app/views/sidebar/history/HistoryV9.tsx b/src/app/views/sidebar/history/HistoryV9.tsx index 7f1becfcb..b2c3bb5e0 100644 --- a/src/app/views/sidebar/history/HistoryV9.tsx +++ b/src/app/views/sidebar/history/HistoryV9.tsx @@ -1,6 +1,10 @@ import { AriaLiveAnnouncer, + Badge, + Button, Divider, + FlatTree, + FlatTreeItem, InputOnChangeData, Label, makeStyles, @@ -8,11 +12,16 @@ import { MessageBarBody, SearchBox, SearchBoxChangeEvent, - Text + Text, + TreeItemLayout, + TreeItemValue, + TreeOpenChangeData, + TreeOpenChangeEvent } from '@fluentui/react-components'; import { IGroup } from '@fluentui/react/lib/DetailsList'; -import { useEffect, useRef, useState } from 'react'; +import { ArrowDownloadRegular } from '@fluentui/react-icons'; +import React, { useEffect, useRef, useState } from 'react'; import { useAppSelector } from '../../../../store'; import { SortOrder } from '../../../../types/enums'; import { IHistoryItem } from '../../../../types/history'; @@ -47,6 +56,10 @@ const useStyles = makeStyles({ } }) +const DownloadHistoryIcon = ()=>{ + return +} + interface HistoryProps { history: IHistoryItem[] groups: IGroup[] @@ -54,8 +67,59 @@ interface HistoryProps { } const History = (props: HistoryProps)=>{ - console.log(props) - return

Items

+ const {groups, history} = props + + const openHistoryItems = new Set() + 'Today'.split('').forEach(ch=> openHistoryItems.add(ch)) + openHistoryItems.add('Today') + + const [openItems, setOpenItems] = useState>( + () => openHistoryItems + ); + const handleOpenChange = (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => { + setOpenItems(data.openItems); + }; + + return( + + {groups.map((group, pos) => { + const historyLeafs = history.slice(group.startIndex, group.startIndex + group.count) + return ( + + + }> + {group.name} + + {group.count} + + + + {openItems.has(group.name) && + historyLeafs.map((h: IHistoryItem) => ( + q.createdAt === h.createdAt) + 1} + > + {h.statusText} + + ))} + + ) + })} + + ) } const sortItems = (content: IHistoryItem[]) => { @@ -145,7 +209,5 @@ export const HistoryV9 = ()=>{ {historyItems.length === 0 && } {`${historyItems.length} search results available.`} -
-
} \ No newline at end of file From 48198879404774cebe860435da408f950d28ae7e Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 27 Nov 2024 16:52:22 +0300 Subject: [PATCH 5/7] feat: add download functionality for history groups in HistoryV9 component --- src/app/views/sidebar/history/HistoryV9.tsx | 77 +++++++++++++++++---- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/src/app/views/sidebar/history/HistoryV9.tsx b/src/app/views/sidebar/history/HistoryV9.tsx index b2c3bb5e0..55e37b1b2 100644 --- a/src/app/views/sidebar/history/HistoryV9.tsx +++ b/src/app/views/sidebar/history/HistoryV9.tsx @@ -13,6 +13,7 @@ import { SearchBox, SearchBoxChangeEvent, Text, + Tooltip, TreeItemLayout, TreeItemValue, TreeOpenChangeData, @@ -20,14 +21,16 @@ import { } from '@fluentui/react-components'; import { IGroup } from '@fluentui/react/lib/DetailsList'; -import { ArrowDownloadRegular } from '@fluentui/react-icons'; +import { ArrowDownloadRegular, DeleteRegular } from '@fluentui/react-icons'; import React, { useEffect, useRef, useState } from 'react'; import { useAppSelector } from '../../../../store'; import { SortOrder } from '../../../../types/enums'; +import { Entry } from '../../../../types/har'; import { IHistoryItem } from '../../../../types/history'; import { dynamicSort } from '../../../utils/dynamic-sort'; import { generateGroupsFromList } from '../../../utils/generate-groups'; import { translateMessage } from '../../../utils/translate-messages'; +import { createHarEntry, exportQuery, generateHar } from './har-utils'; const formatDate = (date: Date) => { const year = date.getFullYear(); @@ -53,11 +56,58 @@ const useStyles = makeStyles({ searchBox: { width: '100%', maxWidth: '100%' + }, + titleAside: { + display: 'flex', + gap: '4px' } }) -const DownloadHistoryIcon = ()=>{ - return + +const handleDeleteHistoryGroup = (event: React.MouseEvent)=>{ + event.preventDefault() + console.log('deleting history') +} + +const handleDownloadHistoryGroup = ( + event: React.MouseEvent, value: string, + historyItems: IHistoryItem[], category: string)=>{ + event.preventDefault() + const itemsToExport = historyItems.filter((query: IHistoryItem) => getCategory(query) === value); + const entries: Entry[] = []; + + itemsToExport.forEach((query: IHistoryItem) => { + const harPayload = createHarEntry(query); + entries.push(harPayload); + }); + + const generatedHarData = generateHar(entries); + const { origin } = new URL(itemsToExport[0].url); + const exportTitle = `${origin}/${category.toLowerCase()}/${itemsToExport[0].createdAt.slice(0, 10)}/`; + + exportQuery(generatedHarData, exportTitle); +} + +interface AsideGroupIconsProps { + groupName: string + historyItems: IHistoryItem[] + category: string +} + +const AsideGroupIcons = (props: AsideGroupIconsProps)=>{ + const {groupName, historyItems, category} = props + const styles = useStyles() + return
+ + + + + + +
} interface HistoryProps { @@ -83,28 +133,29 @@ const History = (props: HistoryProps)=>{ return( {groups.map((group, pos) => { - const historyLeafs = history.slice(group.startIndex, group.startIndex + group.count) + const {name, ariaLabel, count, key, startIndex} = group + const historyLeafs = history.slice(startIndex, startIndex + count) return ( - + - }> - {group.name} - - {group.count} + aria-label={ariaLabel}> + }> + {name}{' '} + + {count} - {openItems.has(group.name) && + {openItems.has(name) && historyLeafs.map((h: IHistoryItem) => ( Date: Wed, 27 Nov 2024 17:14:29 +0300 Subject: [PATCH 6/7] feat: add confirmation dialog for deleting history groups in HistoryV9 component --- src/app/views/sidebar/history/HistoryV9.tsx | 75 ++++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/src/app/views/sidebar/history/HistoryV9.tsx b/src/app/views/sidebar/history/HistoryV9.tsx index 55e37b1b2..f00f403a2 100644 --- a/src/app/views/sidebar/history/HistoryV9.tsx +++ b/src/app/views/sidebar/history/HistoryV9.tsx @@ -2,6 +2,13 @@ import { AriaLiveAnnouncer, Badge, Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, + DialogTrigger, Divider, FlatTree, FlatTreeItem, @@ -23,10 +30,12 @@ import { IGroup } from '@fluentui/react/lib/DetailsList'; import { ArrowDownloadRegular, DeleteRegular } from '@fluentui/react-icons'; import React, { useEffect, useRef, useState } from 'react'; -import { useAppSelector } from '../../../../store'; +import { historyCache } from '../../../../modules/cache/history-utils'; +import { useAppDispatch, useAppSelector } from '../../../../store'; import { SortOrder } from '../../../../types/enums'; import { Entry } from '../../../../types/har'; import { IHistoryItem } from '../../../../types/history'; +import { removeAllHistoryItems } from '../../../services/slices/history.slice'; import { dynamicSort } from '../../../utils/dynamic-sort'; import { generateGroupsFromList } from '../../../utils/generate-groups'; import { translateMessage } from '../../../utils/translate-messages'; @@ -59,19 +68,13 @@ const useStyles = makeStyles({ }, titleAside: { display: 'flex', - gap: '4px' + gap: '2px' } }) - -const handleDeleteHistoryGroup = (event: React.MouseEvent)=>{ - event.preventDefault() - console.log('deleting history') -} - const handleDownloadHistoryGroup = ( event: React.MouseEvent, value: string, - historyItems: IHistoryItem[], category: string)=>{ + historyItems: IHistoryItem[])=>{ event.preventDefault() const itemsToExport = historyItems.filter((query: IHistoryItem) => getCategory(query) === value); const entries: Entry[] = []; @@ -83,7 +86,7 @@ const handleDownloadHistoryGroup = ( const generatedHarData = generateHar(entries); const { origin } = new URL(itemsToExport[0].url); - const exportTitle = `${origin}/${category.toLowerCase()}/${itemsToExport[0].createdAt.slice(0, 10)}/`; + const exportTitle = `${origin}/${value.toLowerCase()}/${itemsToExport[0].createdAt.slice(0, 10)}/`; exportQuery(generatedHarData, exportTitle); } @@ -91,22 +94,58 @@ const handleDownloadHistoryGroup = ( interface AsideGroupIconsProps { groupName: string historyItems: IHistoryItem[] - category: string } const AsideGroupIcons = (props: AsideGroupIconsProps)=>{ - const {groupName, historyItems, category} = props + const dispatch = useAppDispatch() + const {groupName, historyItems} = props + const [open, setOpen] = useState(false); const styles = useStyles() + + const handleDeleteHistoryGroup = (event: React.MouseEvent)=>{ + event.preventDefault() + const itemsToDelete = historyItems.filter((query: IHistoryItem) => getCategory(query) === groupName); + const listOfKeys: string[] = []; + itemsToDelete.forEach(historyItem => { + listOfKeys.push(historyItem.createdAt); + }); + historyCache.bulkRemoveHistoryData(listOfKeys) + dispatch(removeAllHistoryItems(listOfKeys)); + setOpen(false) + } + return
- - - + setOpen(data.open)}> + + + + + + + + {translateMessage('Delete requests')} "{groupName}" + {translateMessage('Are you sure you want to delete these requests?')} + + + + + + + + + + +
} @@ -144,7 +183,7 @@ const History = (props: HistoryProps)=>{ aria-setsize={2} aria-posinset={pos+1} aria-label={ariaLabel}> - }> + }> {name}{' '} {count} From 5b92bcd737babcf37775690df361dcfbd11264e2 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Wed, 27 Nov 2024 18:31:45 +0300 Subject: [PATCH 7/7] feat: add action menu for history items with view, run, export, and delete options --- src/app/views/sidebar/history/HistoryV9.tsx | 197 +++++++++++++++++++- 1 file changed, 194 insertions(+), 3 deletions(-) diff --git a/src/app/views/sidebar/history/HistoryV9.tsx b/src/app/views/sidebar/history/HistoryV9.tsx index f00f403a2..c1b559d4c 100644 --- a/src/app/views/sidebar/history/HistoryV9.tsx +++ b/src/app/views/sidebar/history/HistoryV9.tsx @@ -15,6 +15,13 @@ import { InputOnChangeData, Label, makeStyles, + Menu, + MenuGroup, + MenuGroupHeader, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, MessageBar, MessageBarBody, SearchBox, @@ -28,19 +35,35 @@ import { } from '@fluentui/react-components'; import { IGroup } from '@fluentui/react/lib/DetailsList'; -import { ArrowDownloadRegular, DeleteRegular } from '@fluentui/react-icons'; +import { MessageBarType } from '@fluentui/react'; +import { + ArrowDownloadRegular, + ArrowRepeatAllRegular, + DeleteRegular, + EyeRegular, + MoreHorizontalRegular +} from '@fluentui/react-icons'; import React, { useEffect, useRef, useState } from 'react'; import { historyCache } from '../../../../modules/cache/history-utils'; import { useAppDispatch, useAppSelector } from '../../../../store'; +import { componentNames, eventTypes, telemetry } from '../../../../telemetry'; import { SortOrder } from '../../../../types/enums'; import { Entry } from '../../../../types/har'; import { IHistoryItem } from '../../../../types/history'; -import { removeAllHistoryItems } from '../../../services/slices/history.slice'; +import { IQuery } from '../../../../types/query-runner'; +import { GRAPH_URL } from '../../../services/graph-constants'; +import { runQuery, setQueryResponse } from '../../../services/slices/graph-response.slice'; +import { removeAllHistoryItems, removeHistoryItem } from '../../../services/slices/history.slice'; +import { setQueryResponseStatus } from '../../../services/slices/query-status.slice'; +import { setSampleQuery } from '../../../services/slices/sample-query.slice'; import { dynamicSort } from '../../../utils/dynamic-sort'; import { generateGroupsFromList } from '../../../utils/generate-groups'; +import { sanitizeQueryUrl } from '../../../utils/query-url-sanitization'; +import { parseSampleUrl } from '../../../utils/sample-url-generation'; import { translateMessage } from '../../../utils/translate-messages'; import { createHarEntry, exportQuery, generateHar } from './har-utils'; +type BadgeColors = 'brand' | 'danger' | 'important' | 'informative' | 'severe' | 'subtle' | 'success' | 'warning'; const formatDate = (date: Date) => { const year = date.getFullYear(); const month = date.getMonth() + 1; @@ -156,6 +179,7 @@ interface HistoryProps { } const History = (props: HistoryProps)=>{ + const dispatch = useAppDispatch() const {groups, history} = props const openHistoryItems = new Set() @@ -169,6 +193,38 @@ const History = (props: HistoryProps)=>{ setOpenItems(data.openItems); }; + const handleViewQuery = (query: IHistoryItem)=>{ + const { sampleUrl, queryVersion } = parseSampleUrl(query.url); + const sampleQuery: IQuery = { + sampleUrl, + selectedVerb: query.method, + sampleBody: query.body, + sampleHeaders: query.headers, + selectedVersion: queryVersion + }; + const { duration, status, statusText } = query; + dispatch(setSampleQuery(sampleQuery)); + dispatch(setQueryResponse({ + body: query.result, + headers: query.responseHeaders + })) + // TODO: change the message bar type to v9 types + dispatch(setQueryResponseStatus({ + duration, + messageType: + status < 300 ? MessageBarType.success : MessageBarType.error, + ok: status < 300, + status, + statusText + })); + trackHistoryItemEvent( + eventTypes.LISTITEM_CLICK_EVENT, + componentNames.HISTORY_LIST_ITEM, + query + ); + } + + return( {groups.map((group, pos) => { @@ -202,7 +258,14 @@ const History = (props: HistoryProps)=>{ aria-setsize={historyLeafs.length} aria-posinset={historyLeafs.findIndex((q) => q.createdAt === h.createdAt) + 1} > - {h.statusText} + handleViewQuery(h)} + iconBefore={} + aside={}> + + {h.url.replace(GRAPH_URL, '')} + +
))}
@@ -212,6 +275,134 @@ const History = (props: HistoryProps)=>{ ) } +const HistoryStatusCodes = ({status}:{status: number})=>{ + const getBadgeColor = (): BadgeColors =>{ + if(status >= 100 && status < 199) {return 'informative'} + if(status >= 200 && status < 299) {return 'success'} + if(status >= 300 && status < 399) {return 'important'} + if(status >= 400 && status < 599) {return 'danger'} + return 'success' + } + return {status} +} + +const trackHistoryItemEvent = (eventName: string, componentName: string, query: IHistoryItem) => { + const sanitizedUrl = sanitizeQueryUrl(query.url); + telemetry.trackEvent( + eventName, + { + ComponentName: componentName, + ItemIndex: query.index, + QuerySignature: `${query.method} ${sanitizedUrl}` + }); +} + +interface HistoryItemActionMenuProps { + item: IHistoryItem +} + + +const HistoryItemActionMenu = (props: HistoryItemActionMenuProps)=>{ + const dispatch = useAppDispatch() + const {item} = props + + const handleViewQuery = (query: IHistoryItem)=>{ + const { sampleUrl, queryVersion } = parseSampleUrl(query.url); + const sampleQuery: IQuery = { + sampleUrl, + selectedVerb: query.method, + sampleBody: query.body, + sampleHeaders: query.headers, + selectedVersion: queryVersion + }; + const { duration, status, statusText } = query; + dispatch(setSampleQuery(sampleQuery)); + dispatch(setQueryResponse({ + body: query.result, + headers: query.responseHeaders + })) + // TODO: change the message bar type to v9 types + dispatch(setQueryResponseStatus({ + duration, + messageType: + status < 300 ? MessageBarType.success : MessageBarType.error, + ok: status < 300, + status, + statusText + })); + trackHistoryItemEvent( + eventTypes.LISTITEM_CLICK_EVENT, + componentNames.HISTORY_LIST_ITEM, + query + ); + } + + const handleRunQuery = (query: IHistoryItem) =>{ + const { sampleUrl, queryVersion } = parseSampleUrl(query.url); + const sampleQuery: IQuery = { + sampleUrl, + selectedVerb: query.method, + sampleBody: query.body, + sampleHeaders: query.headers, + selectedVersion: queryVersion + }; + + if (sampleQuery.selectedVerb === 'GET') { + sampleQuery.sampleBody = JSON.parse('{}') as string; + } + dispatch(setSampleQuery(sampleQuery)); + dispatch(runQuery(sampleQuery)); + + trackHistoryItemEvent( + eventTypes.BUTTON_CLICK_EVENT, + componentNames.RUN_HISTORY_ITEM_BUTTON, + query + ); + } + + const handleExportQuery = (query: IHistoryItem)=>{ + const harPayload = createHarEntry(query); + const generatedHarData = generateHar([harPayload]); + exportQuery(generatedHarData, `${query.url}/`); + trackHistoryItemEvent( + eventTypes.BUTTON_CLICK_EVENT, + componentNames.EXPORT_HISTORY_ITEM_BUTTON, + query + ); + } + + const handleDeleteQuery = (query:IHistoryItem)=>{ + delete query.category; + historyCache.removeHistoryData(query); + dispatch(removeHistoryItem(query)); + trackHistoryItemEvent( + eventTypes.BUTTON_CLICK_EVENT, + componentNames.DELETE_HISTORY_ITEM_BUTTON, + query + ); + } + return + + + + + + + + {translateMessage('actions')} + } onClick={()=>handleViewQuery(item)}>{translateMessage('view')} + } + onClick={()=>handleRunQuery(item)}>{translateMessage('Run Query')} + } + onClick={()=>handleExportQuery(item)}>{translateMessage('Export')} + } + onClick={()=>handleDeleteQuery(item)}>{translateMessage('Delete')} + + + + +} + const sortItems = (content: IHistoryItem[]) => { content.sort(dynamicSort('createdAt', SortOrder.DESC)); content.forEach((value: IHistoryItem, index: number) => {