Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add hook for url filters and few common constants #78

Merged
merged 11 commits into from
Feb 8, 2024
Merged
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtron-labs/devtron-fe-common-lib",
"version": "0.0.58",
"version": "0.0.58-beta-9",
"description": "Supporting common component library",
"main": "dist/index.js",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-arrow-up-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-sort-arrow-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/Common/Api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ServerErrors } from './ServerError'
import { FALLBACK_REQUEST_TIMEOUT, Host, URLS } from './Constants'
import { ResponseType, APIOptions } from './Types'
import { MutableRefObject } from 'react'

const responseMessages = {
100: 'Continue',
Expand Down Expand Up @@ -228,3 +229,23 @@ export const get = (url: string, options?: APIOptions): Promise<ResponseType> =>
export const trash = (url: string, data?: object, options?: APIOptions): Promise<ResponseType> => {
return fetchInTime(url, 'DELETE', data, options)
}

/**
* Aborts the previous request before triggering next request
*/
export const abortPreviousRequests = <T,>(
callback: () => Promise<T>,
abortControllerRef: MutableRefObject<AbortController>,
): Promise<T> => {
abortControllerRef.current.abort()
// eslint-disable-next-line no-param-reassign
abortControllerRef.current = new AbortController()
return callback()
}

/**
* Returns true if the error is due to a aborted request
*/
export const getIsRequestAborted = (error) =>
// The 0 code is common for aborted and blocked requests
error && error.code === 0 && error.message === 'The user aborted a request.'
18 changes: 17 additions & 1 deletion src/Common/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,20 @@ export const MAX_Z_INDEX = 2147483647
export const SELECTED_APPROVAL_TAB_STATE = {
APPROVAL: 'approval',
PENDING: 'pending',
}
}

export enum SortingOrder {
/**
* Ascending order
*/
ASC = 'ASC',
/**
* Descending order
*/
DESC = 'DESC',
}

/**
* Base page size for pagination
*/
export const DEFAULT_BASE_PAGE_SIZE = 20
1 change: 1 addition & 0 deletions src/Common/Hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useSuperAdmin } from './UseSuperAdmin/UseSuperAdmin'
export { useClickOutside } from './UseClickOutside/UseClickOutside'
export { useWindowSize } from './UseWindowSize/UseWindowSize'
export * from './useUrlFilters'
9 changes: 9 additions & 0 deletions src/Common/Hooks/useUrlFilters/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const DEFAULT_PAGE_NUMBER = 1

export const URL_FILTER_KEYS = {
PAGE_SIZE: 'pageSize',
PAGE_NUMBER: 'pageNumber',
SEARCH_KEY: 'searchKey',
SORT_BY: 'sortBy',
SORT_ORDER: 'sortOrder',
} as const
2 changes: 2 additions & 0 deletions src/Common/Hooks/useUrlFilters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as useUrlFilters } from './useUrlFilters'
export type { UseUrlFiltersProps } from './types'
6 changes: 6 additions & 0 deletions src/Common/Hooks/useUrlFilters/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface UseUrlFiltersProps<T> {
/**
* The key on which the sorting should be applied
*/
initialSortKey?: T
}
120 changes: 120 additions & 0 deletions src/Common/Hooks/useUrlFilters/useUrlFilters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useMemo } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { DEFAULT_BASE_PAGE_SIZE, SortingOrder } from '../../Constants'
import { DEFAULT_PAGE_NUMBER, URL_FILTER_KEYS } from './constants'
import { UseUrlFiltersProps } from './types'

const { PAGE_SIZE, PAGE_NUMBER, SEARCH_KEY, SORT_BY, SORT_ORDER } = URL_FILTER_KEYS

/**
* Generic hook for implementing URL based filters.
* eg: pagination, search, sort.
*
* The exposed handlers can be consumed directly without the need for explicit state management
*
* @example Default Usage:
* ```tsx
* const { pageSize, changePage, ...rest } = useUrlFilters()
* ```
*
* @example Usage with custom type for sort keys and initial sort key:
* ```tsx
* const { sortBy, sortOrder } = useUrlFilters<'email' | 'name'>({ initialSortKey: 'email' })
* ```
*
*/
const useUrlFilters = <T = string>({ initialSortKey }: UseUrlFiltersProps<T> = {}) => {
const location = useLocation()
const history = useHistory()
const searchParams = new URLSearchParams(location.search)

const { pageSize, pageNumber, searchKey, sortBy, sortOrder } = useMemo(() => {
const _pageSize = searchParams.get(PAGE_SIZE)
const _pageNumber = searchParams.get(PAGE_NUMBER)
const _searchKey = searchParams.get(SEARCH_KEY)
const _sortOrder = searchParams.get(SORT_ORDER) as SortingOrder
const _sortBy = searchParams.get(SORT_BY)

const sortByKey = (_sortBy || initialSortKey || '') as T
// Fallback to ascending order
const sortByOrder = Object.values(SortingOrder).includes(_sortOrder) ? _sortOrder : SortingOrder.ASC

return {
pageSize: Number(_pageSize) || DEFAULT_BASE_PAGE_SIZE,
pageNumber: Number(_pageNumber) || DEFAULT_PAGE_NUMBER,
searchKey: _searchKey || '',
sortBy: sortByKey,
// sort order should only be applied if the key is available
sortOrder: (sortByKey ? sortByOrder : '') as SortingOrder,
}
}, [searchParams])

/**
* Used for getting the required result from the API
*/
const offset = pageSize * (pageNumber - 1)

/**
* Update and replace the search params in the URL.
*
* Note: Currently only primitive data types are supported
*/
const _updateSearchParam = (key: string, value) => {
searchParams.set(key, String(value))
history.replace({ search: searchParams.toString() })
}

const _resetPageNumber = () => {
_updateSearchParam(PAGE_NUMBER, DEFAULT_PAGE_NUMBER)
}

const changePage = (page: number) => {
_updateSearchParam(PAGE_NUMBER, page)
}

const changePageSize = (_pageSize: number) => {
_updateSearchParam(PAGE_SIZE, _pageSize)
_resetPageNumber()
}

const handleSearch = (searchTerm: string) => {
_updateSearchParam(SEARCH_KEY, searchTerm)
}

const handleSorting = (_sortBy: T) => {
let order: SortingOrder
if (_sortBy === sortBy && sortOrder === SortingOrder.ASC) {
order = SortingOrder.DESC
} else {
order = SortingOrder.ASC
}

_updateSearchParam(SORT_BY, _sortBy)
_updateSearchParam(SORT_ORDER, order)

// Reset page number on sorting change
_resetPageNumber()
}

const clearFilters = () => {
Object.values(URL_FILTER_KEYS).forEach((key) => {
searchParams.delete(key)
})
history.replace({ search: searchParams.toString() })
}

return {
pageSize,
changePage,
changePageSize,
searchKey,
handleSearch,
offset,
sortBy,
sortOrder,
handleSorting,
clearFilters,
}
}

export default useUrlFilters
44 changes: 44 additions & 0 deletions src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ReactComponent as SortIcon } from '../../Assets/Icon/ic-arrow-up-down.svg'
import { ReactComponent as SortArrowDown } from '../../Assets/Icon/ic-sort-arrow-down.svg'
import { SortingOrder } from '../Constants'
import { SortableTableHeaderCellProps } from './types'

/**
* Reusable component for the table header cell with support for sorting icons
*
* @example Usage
* ```tsx
* <SortableTableHeaderCell
* isSorted={currentSortedCell === 'cell'}
* triggerSorting={() => {}}
* sortOrder={SortingOrder.ASC}
* title="Header Cell"
* disabled={isDisabled}
* />
* ```
*/
const SortableTableHeaderCell = ({
isSorted,
triggerSorting,
sortOrder,
title,
disabled,
}: SortableTableHeaderCellProps) => (
<button
type="button"
className="dc__transparent p-0 bcn-0 cn-7 flex dc__content-start dc__gap-4 cursor"
onClick={triggerSorting}
disabled={disabled}
>
<span className="dc__uppercase dc__ellipsis-right">{title}</span>
{isSorted ? (
<SortArrowDown
className={`icon-dim-12 mw-12 scn-7 ${sortOrder === SortingOrder.DESC ? 'dc__flip-180' : ''}`}
/>
) : (
<SortIcon className="icon-dim-12 mw-12 scn-7" />
)}
</button>
)

export default SortableTableHeaderCell
2 changes: 2 additions & 0 deletions src/Common/SortableTableHeaderCell/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as SortableTableHeaderCell } from './SortableTableHeaderCell'
export type { SortableTableHeaderCellProps } from './types'
26 changes: 26 additions & 0 deletions src/Common/SortableTableHeaderCell/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SortingOrder } from '../Constants'

export interface SortableTableHeaderCellProps {
/**
* If true, the cell is sorted
*/
isSorted: boolean
/**
* Callback for handling the sorting of the cell
*/
triggerSorting: () => void
/**
* Current sort order
*
* Note: On click, the sort order should be updated as required
*/
sortOrder: SortingOrder
/**
* Label for the cell
*/
title: string
/**
* If true, the cell is disabled
*/
disabled: boolean
}
14 changes: 13 additions & 1 deletion src/Common/Types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ReactNode, CSSProperties } from 'react'
import { Placement } from 'tippy.js'
import { ImageComment, ReleaseTag } from './ImageTags.Types'
import { DockerConfigOverrideType, TaskErrorObj } from '.'
import { DockerConfigOverrideType, SortingOrder, TaskErrorObj } from '.'

/**
* Generic response type object with support for overriding the result type
Expand Down Expand Up @@ -762,3 +762,15 @@ export interface EdgeNodeType {
export interface EdgeEndNodeType extends EdgeNodeType {
userApprovalConfig?: UserApprovalConfigType
}

/**
* Search params for sorting configuration
*
* Note: Either both sortOrder and sortBy are required or none
*/
export type SortingParams<T = string> =
| {
sortOrder: SortingOrder
sortBy: T
}
| { sortOrder?: never; sortBy?: never }
1 change: 1 addition & 0 deletions src/Common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './Checkbox'
export { default as EmptyState } from './EmptyState/EmptyState'
export { default as GenericEmptyState } from './EmptyState/GenericEmptyState'
export * from './SearchBar'
export * from './SortableTableHeaderCell'
export { default as Toggle } from './Toggle/Toggle'
export { default as ScanVulnerabilitiesTable } from './Security/ScanVulnerabilitiesTable'
export { default as StyledRadioGroup } from './RadioGroup/RadioGroup'
Expand Down
Loading