diff --git a/package.json b/package.json
index 9269da9ed..659f98861 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/Assets/Icon/ic-error-cross.svg b/src/Assets/Icon/ic-error-cross.svg
new file mode 100644
index 000000000..ea48072bf
--- /dev/null
+++ b/src/Assets/Icon/ic-error-cross.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/Common/DebouncedSearch/DebouncedSearch.tsx b/src/Common/DebouncedSearch/DebouncedSearch.tsx
new file mode 100644
index 000000000..bbf38c541
--- /dev/null
+++ b/src/Common/DebouncedSearch/DebouncedSearch.tsx
@@ -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('')
+
+ useEffect(() => {
+ setSearchText('')
+ }, [clearSearch])
+
+ const handleSearchTextChange = (e: React.ChangeEvent) => {
+ setSearchText(e.target.value)
+ }
+
+ const handleClearSearch = () => {
+ setSearchText('')
+ }
+
+ useDebouncedEffect(() => onSearch(searchText), debounceTimeout, [searchText, onSearch])
+
+ return (
+
+ {Icon && }
+
+
+
+ {showClearIcon && !!searchText && (
+
+ )}
+
+ {/* In case we want to add another button or something */}
+ {children}
+
+ )
+}
diff --git a/src/Common/DebouncedSearch/Types.ts b/src/Common/DebouncedSearch/Types.ts
new file mode 100644
index 000000000..972d8a407
--- /dev/null
+++ b/src/Common/DebouncedSearch/Types.ts
@@ -0,0 +1,12 @@
+export interface DebouncedSearchProps {
+ onSearch: (query: string) => void
+ Icon?: React.FunctionComponent>
+ placeholder?: string
+ inputClass?: string
+ containerClass?: string
+ iconClass?: string
+ children?: React.ReactNode
+ debounceTimeout?: number
+ clearSearch?: boolean
+ showClearIcon?: boolean
+}
diff --git a/src/Common/DebouncedSearch/Utils.ts b/src/Common/DebouncedSearch/Utils.ts
new file mode 100644
index 000000000..1891ca56c
--- /dev/null
+++ b/src/Common/DebouncedSearch/Utils.ts
@@ -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])
+}
diff --git a/src/Common/DebouncedSearch/__tests__/DebouncedSearch.test.tsx b/src/Common/DebouncedSearch/__tests__/DebouncedSearch.test.tsx
new file mode 100644
index 000000000..9df596f1f
--- /dev/null
+++ b/src/Common/DebouncedSearch/__tests__/DebouncedSearch.test.tsx
@@ -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()
+ expect(screen.getByRole('textbox')).toBeTruthy()
+ })
+
+ it('should have a clear icon when clearSearch is true and user has typed something', () => {
+ const { getByTestId } = render()
+ 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()
+ fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test' } })
+ expect(onSearch).toHaveBeenCalled()
+ })
+
+ it('should clear input value when clear icon is clicked', () => {
+ const { container } = render()
+ 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()
+ expect(screen.getByPlaceholderText('test')).toBeTruthy()
+ })
+
+ it('should not show clear icon when showClearIcon is false', () => {
+ render()
+ expect(screen.queryByRole('button')).toBeNull()
+ })
+
+ it('should have a custom Icon', () => {
+ const { container } = render()
+ expect(container.querySelector('.icon-class')).toBeTruthy()
+ })
+
+ it('should support children', () => {
+ render(
+
+
test
+ ,
+ )
+ expect(screen.getByText('test')).toBeTruthy()
+ })
+})
diff --git a/src/Common/Grid/Grid.tsx b/src/Common/Grid/Grid.tsx
new file mode 100644
index 000000000..108a7c50c
--- /dev/null
+++ b/src/Common/Grid/Grid.tsx
@@ -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 (
+
+ {children}
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/Common/Grid/types.ts b/src/Common/Grid/types.ts
new file mode 100644
index 000000000..82bbce7ca
--- /dev/null
+++ b/src/Common/Grid/types.ts
@@ -0,0 +1,9 @@
+export interface GridProps {
+ container?: boolean
+ spacing?: number
+ item?: boolean
+ xs?: number
+ containerClass?: string
+ itemClass?: string
+ children?: React.ReactNode
+}
diff --git a/src/Common/index.ts b/src/Common/index.ts
index d79e17934..b7ac8d78e 100644
--- a/src/Common/index.ts
+++ b/src/Common/index.ts
@@ -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'
\ No newline at end of file
+export * from './ResizableTextarea'
+export { default as DebouncedSearch } from './DebouncedSearch/DebouncedSearch'
+export { default as Grid } from './Grid/Grid'