Skip to content

Commit

Permalink
Merge pull request #29 from devtron-labs/feat/dashboard-common
Browse files Browse the repository at this point in the history
Feat: dashboard common
  • Loading branch information
AbhishekA1509 authored Sep 27, 2023
2 parents f7caf35 + 666f5bf commit 8c71936
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 2 deletions.
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.26",
"version": "0.0.27",
"description": "Supporting common component library",
"main": "dist/index.js",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions src/Assets/Icon/ic-error-cross.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions src/Common/DebouncedSearch/DebouncedSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react'
import { useDebouncedEffect } from './Utils'
import { ReactComponent as ICClear } from '../../Assets/Icon/ic-error-cross.svg'
import { DebouncedSearchProps } from './Types'

/**
* @param onSearch - Callback function to be called on search
* @param Icon - (Optional) Icon to be shown before the input
* @param iconClass - (Optional) Class for the icon
* @param children - (Optional) In case we want to add another button or any other element
* @param placeholder - (Optional) Placeholder for the input
* @param containerClass - (Optional) Class for the container
* @param inputClass - (Optional) Class for the input field
* @param debounceTimeout - (Optional) Timeout for the debounce with default value of 500ms
* @param clearSearch - (Optional) To clear the search text
* @param showClearIcon - (Optional) To show the clear icon default value is true
*/
export default function DebouncedSearch({
onSearch,
Icon,
children,
placeholder,
containerClass = '',
iconClass = '',
inputClass = '',
debounceTimeout = 500,
clearSearch,
showClearIcon = true,
}: DebouncedSearchProps) {
const [searchText, setSearchText] = useState<string>('')

useEffect(() => {
setSearchText('')
}, [clearSearch])

const handleSearchTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value)
}

const handleClearSearch = () => {
setSearchText('')
}

useDebouncedEffect(() => onSearch(searchText), debounceTimeout, [searchText, onSearch])

return (
<div className={containerClass}>
{Icon && <Icon className={iconClass} />}

<input
type="text"
className={inputClass}
placeholder={placeholder ?? 'Search'}
value={searchText}
onChange={handleSearchTextChange}
autoFocus
data-testid="debounced-search"
/>

{showClearIcon && !!searchText && (
<button
type="button"
className="dc__outline-none-imp dc__no-border p-0 bc-n50 flex"
onClick={handleClearSearch}
data-testid="clear-search"
>
<ICClear className="icon-dim-20 icon-n6" />
</button>
)}

{/* In case we want to add another button or something */}
{children}
</div>
)
}
12 changes: 12 additions & 0 deletions src/Common/DebouncedSearch/Types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface DebouncedSearchProps {
onSearch: (query: string) => void
Icon?: React.FunctionComponent<React.SVGProps<SVGSVGElement>>
placeholder?: string
inputClass?: string
containerClass?: string
iconClass?: string
children?: React.ReactNode
debounceTimeout?: number
clearSearch?: boolean
showClearIcon?: boolean
}
19 changes: 19 additions & 0 deletions src/Common/DebouncedSearch/Utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useRef } from 'react'

export function useDebouncedEffect(callback, delay, deps: unknown[] = []) {
// function will be executed only after the specified time once the user stops firing the event.
const firstUpdate = useRef(true)
useEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false
return
}
const handler = setTimeout(() => {
callback()
}, delay)

return () => {
clearTimeout(handler)
}
}, [delay, ...deps])
}
61 changes: 61 additions & 0 deletions src/Common/DebouncedSearch/__tests__/DebouncedSearch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { ReactComponent as ICClear } from '../../../../assets/icons/ic-error.svg'
import DebouncedSearch from '../DebouncedSearch'

jest.mock('../../helpers/Helpers', () => ({
useDebouncedEffect: jest.fn().mockImplementation((fn) => fn()),
}))

describe('When DebouncedSearch mounts', () => {
it('should have a input field', () => {
render(<DebouncedSearch onSearch={jest.fn()} />)
expect(screen.getByRole('textbox')).toBeTruthy()
})

it('should have a clear icon when clearSearch is true and user has typed something', () => {
const { getByTestId } = render(<DebouncedSearch onSearch={jest.fn()} clearSearch debounceTimeout={0} />)
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test' } })
expect(getByTestId('clear-search')).toBeTruthy()
})

it('should call onSearch when input value changes', () => {
const onSearch = jest.fn()
render(<DebouncedSearch onSearch={onSearch} debounceTimeout={0} />)
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test' } })
expect(onSearch).toHaveBeenCalled()
})

it('should clear input value when clear icon is clicked', () => {
const { container } = render(<DebouncedSearch onSearch={jest.fn()} clearSearch debounceTimeout={0} />)
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test' } })
const inputBar = container.querySelector('input')
expect(inputBar.value).toBe('test')
fireEvent.click(screen.getByRole('button'))
expect(inputBar.value).toBe('')
})

it('should have a placeholder', () => {
render(<DebouncedSearch onSearch={jest.fn()} placeholder="test" />)
expect(screen.getByPlaceholderText('test')).toBeTruthy()
})

it('should not show clear icon when showClearIcon is false', () => {
render(<DebouncedSearch onSearch={jest.fn()} showClearIcon={false} />)
expect(screen.queryByRole('button')).toBeNull()
})

it('should have a custom Icon', () => {
const { container } = render(<DebouncedSearch onSearch={jest.fn()} Icon={ICClear} iconClass="icon-class" />)
expect(container.querySelector('.icon-class')).toBeTruthy()
})

it('should support children', () => {
render(
<DebouncedSearch onSearch={jest.fn()}>
<div>test</div>
</DebouncedSearch>,
)
expect(screen.getByText('test')).toBeTruthy()
})
})
39 changes: 39 additions & 0 deletions src/Common/Grid/Grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react'
import { GridProps } from './types'

// This is meant to be a reusable component that will provide a grid like dynamic layout where xs is the number of columns out of 12 that the item will take up
export default function Grid({
container,
spacing = 0,
item,
xs,
containerClass = '',
itemClass = '',
children,
}: GridProps) {
const containerStyles = container ? { gap: spacing + 'px' } : {}

if (item) {
const getColumnWidth = () => {
const percentageWidth = (xs / 12) * 100
// DONT CHANGE IT FROM CALC SINCE CALC CONVERTS TO PX which is needed to handle text overflow
return `calc(${percentageWidth}%)`
}

const itemStyles = {
flex: `1 1 ${getColumnWidth()}`,
}

return (
<div className={`p-0 ${itemClass}`} style={itemStyles}>
{children}
</div>
)
}

return (
<div className={`flex-wrap flexbox ${container ? containerClass : ''}`} style={containerStyles}>
{children}
</div>
)
}
9 changes: 9 additions & 0 deletions src/Common/Grid/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface GridProps {
container?: boolean
spacing?: number
item?: boolean
xs?: number
containerClass?: string
itemClass?: string
children?: React.ReactNode
}
4 changes: 3 additions & 1 deletion src/Common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ export * from './Policy.Types'
export { default as DeleteComponent } from './DeleteComponentModal/DeleteComponent'
export * from './ImageTags'
export * from './ImageTags.Types'
export * from './ResizableTextarea'
export * from './ResizableTextarea'
export { default as DebouncedSearch } from './DebouncedSearch/DebouncedSearch'
export { default as Grid } from './Grid/Grid'

0 comments on commit 8c71936

Please sign in to comment.