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: common functionalities from dashboard #34

Merged
merged 3 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.30",
"version": "0.0.31",
"description": "Supporting common component library",
"main": "dist/index.js",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions src/Common/ClipboardButton/ClipboardButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState, useEffect, useCallback } from 'react'
import Tippy from '@tippyjs/react'
import { copyToClipboard } from '../Helper'
import ClipboardProps from './types'
import { ReactComponent as ICCopy } from '../../Assets/Icon/ic-copy.svg'

/**
* @param content - Content to be copied
* @param copiedTippyText - Text to be shown in the tippy when the content is copied
* @param duration - Duration for which the tippy should be shown
* @param trigger - To trigger the copy action, if set to true the content will be copied, use case being triggering the copy action from outside the component
* @param setTrigger - Callback function to set the trigger
*/
export default function ClipboardButton({ content, copiedTippyText, duration, trigger, setTrigger }: ClipboardProps) {
const [copied, setCopied] = useState<boolean>(false)
const [enableTippy, setEnableTippy] = useState<boolean>(false)

const handleTextCopied = () => setCopied(true)
const handleEnableTippy = () => setEnableTippy(true)
const handleDisableTippy = () => setEnableTippy(false)
const handleCopyContent = useCallback(() => copyToClipboard(content, handleTextCopied), [content])

useEffect(() => {
if (!copied) return

const timeout = setTimeout(() => {
setCopied(false)
setTrigger(false)
}, duration)

return () => clearTimeout(timeout)
}, [copied, duration, setTrigger])

useEffect(() => {
if (trigger) {
setCopied(true)
handleCopyContent()
}
}, [trigger, handleCopyContent])

return (
<div className="icon-dim-16 flex center">
<Tippy
className="default-tt"
content={copied ? copiedTippyText : 'Copy'}
placement="right"
visible={copied || enableTippy}
>
<button
type="button"
className="dc__hover-n100 dc__outline-none-imp p-0 flex bcn-0 dc__no-border"
onMouseEnter={handleEnableTippy}
onMouseLeave={handleDisableTippy}
onClick={handleCopyContent}
>
<ICCopy className="icon-dim-16" />
</button>
</Tippy>
</div>
)
}
43 changes: 43 additions & 0 deletions src/Common/ClipboardButton/__tests__/ClipboardButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import ClipboardButton from '../ClipboardButton'

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

describe('When ClipboardButton mounts', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should show the copy icon', () => {
render(
<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
)
expect(screen.getByRole('button')).toBeTruthy()
})

it('should show tippy on hover', () => {
render(
<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
)
fireEvent.mouseEnter(screen.getByRole('button'))
expect(screen.getByText('Copy')).toBeTruthy()
})

it('should show copiedTippyText when trigger is true', () => {
render(<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger setTrigger={null} />)
expect(screen.getByText('test')).toBeTruthy()
})

it('should call copyToClipboard when clicked', () => {
render(
<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByText('test')).toBeTruthy()
})
})
7 changes: 7 additions & 0 deletions src/Common/ClipboardButton/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default interface ClipboardProps {
content: string
copiedTippyText: string
duration: number
trigger: boolean
setTrigger: React.Dispatch<React.SetStateAction<boolean>>
}
149 changes: 131 additions & 18 deletions src/Common/Helper.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ServerErrors } from './ServerError'
import { useLocation } from 'react-router-dom'
import moment from 'moment'
import { toast } from 'react-toastify'
import * as Sentry from '@sentry/browser'
import { ServerErrors } from './ServerError'
import { toastAccessDenied } from './ToastBody'
import { ERROR_EMPTY_SCREEN, TOKEN_COOKIE_NAME } from './Constants'
import { ReactComponent as FormError } from '../Assets/Icon/ic-warning.svg'
import moment from 'moment';
import { UseSearchString } from './Types'
import { useLocation } from 'react-router-dom'
import { AsyncOptions, AsyncState, UseSearchString } from './Types'

toast.configure({
autoClose: 3000,
Expand Down Expand Up @@ -336,17 +336,17 @@ export function CustomInput({
}

export function handleUTCTime(ts: string, isRelativeTime = false) {
let timestamp = "";
try {
if (ts && ts.length) {
let date = moment(ts);
if (isRelativeTime) timestamp = date.fromNow();
else timestamp = date.format("ddd DD MMM YYYY HH:mm:ss");
}
} catch (error) {
console.error("Error Parsing Date:", ts);
}
return timestamp;
let timestamp = ''
try {
if (ts && ts.length) {
let date = moment(ts)
if (isRelativeTime) timestamp = date.fromNow()
else timestamp = date.format('ddd DD MMM YYYY HH:mm:ss')
}
} catch (error) {
console.error('Error Parsing Date:', ts)
}
return timestamp
}

export function useSearchString(): UseSearchString {
Expand All @@ -368,9 +368,122 @@ export function useSearchString(): UseSearchString {
return { queryParams, searchParams }
}


export const closeOnEscKeyPressed = (e: any, actionClose: () => void) => {
if (e.keyCode === 27 || e.key === 'Escape') {
if (e.keyCode === 27 || e.key === 'Escape') {
actionClose()
}
}
}

const unsecureCopyToClipboard = (str, callback = noop) => {
const listener = function (ev) {
ev.preventDefault()
ev.clipboardData.setData('text/plain', str)
}
document.addEventListener('copy', listener)
document.execCommand('copy')
document.removeEventListener('copy', listener)
callback()
}

/**
* It will copy the passed content to clipboard and invoke the callback function, in case of error it will show the toast message.
* On HTTP system clipboard is not supported, so it will use the unsecureCopyToClipboard function
* @param str
* @param callback
*/
export function copyToClipboard(str, callback = noop) {
if (!str) {
return
}

if (window.isSecureContext && navigator.clipboard) {
navigator.clipboard
.writeText(str)
.then(() => {
callback()
})
.catch(() => {
toast.error('Failed to copy to clipboard')
})
} else {
unsecureCopyToClipboard(str, callback)
}
}

export function useAsync<T>(
func: (...rest) => Promise<T>,
dependencyArray: any[] = [],
shouldRun = true,
options: AsyncOptions = { resetOnChange: true },
): [boolean, T, any | null, () => void, React.Dispatch<any>, any[]] {
const [state, setState] = useState<AsyncState<T>>({
loading: true,
result: null,
error: null,
dependencies: dependencyArray,
})
const mounted = useRef(true)
const dependencies: any[] = useMemo(() => {
return [...dependencyArray, shouldRun]
}, [...dependencyArray, shouldRun])

const reload = () => {
async function call() {
try {
setState((state) => ({
...state,
loading: true,
}))
const result = await func()
if (mounted.current)
setState((state) => ({
...state,
result,
error: null,
loading: false,
}))
} catch (error: any) {
if (mounted.current)
setState((state) => ({
...state,
error,
loading: false,
}))
}
}
call()
}

useEffect(() => {
if (!shouldRun) {
setState((state) => ({ ...state, loading: false }))
return
}
setState((state) => ({ ...state, dependencies: dependencyArray }))
reload()
return () =>
setState((state) => ({
...state,
loading: false,
error: null,
...(options.resetOnChange ? { result: null } : {}),
}))
}, dependencies)

useEffect(() => {
mounted.current = true
return () => {
mounted.current = false
}
}, [])

const setResult = (param) => {
if (typeof param === 'function') {
setState((state) => ({ ...state, result: param(state.result) }))
} else {
setState((state) => ({ ...state, result: param }))
}
}

return [state.loading, state.result, state.error, reload, setResult, state.dependencies]
}
14 changes: 13 additions & 1 deletion src/Common/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,16 @@ export interface ResizableTextareaProps {
disabled?: boolean
name?: string
dataTestId?: string
}
}

export interface AsyncState<T> {
loading: boolean
result: T
error: null
dependencies: any[]
}


export interface AsyncOptions {
resetOnChange: boolean
}
1 change: 1 addition & 0 deletions src/Common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ export * from './ImageTags.Types'
export * from './ResizableTextarea'
export { default as DebouncedSearch } from './DebouncedSearch/DebouncedSearch'
export { default as Grid } from './Grid/Grid'
export { default as ClipboardButton } from './ClipboardButton/ClipboardButton'