@devtron-labs/devtron-fe-common-lib 0.0.30 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devtron-labs/devtron-fe-common-lib",
3
- "version": "0.0.30",
3
+ "version": "0.0.31",
4
4
  "description": "Supporting common component library",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
2
+ <path fill="#767D84" fill-rule="evenodd" d="M15.833 6.917a2.5 2.5 0 0 1 2.5 2.5v6.416a2.5 2.5 0 0 1-2.5 2.5H9.417a2.5 2.5 0 0 1-2.5-2.5V9.417a2.5 2.5 0 0 1 2.5-2.5zm0 1.666H9.417a.833.833 0 0 0-.834.834v6.416c0 .46.373.834.834.834h6.416c.46 0 .834-.373.834-.834V9.417a.833.833 0 0 0-.834-.834zM10.75 1.667c1.24 0 2.256.968 2.33 2.19l.003.143v.75a.833.833 0 0 1-1.66.097l-.006-.097V4c0-.34-.255-.62-.583-.661l-.084-.006H4c-.34 0-.62.255-.661.583L3.333 4v6.75c0 .34.255.62.583.661l.084.006h.75a.833.833 0 0 1 .097 1.66l-.097.006H4a2.333 2.333 0 0 1-2.33-2.19l-.003-.143V4c0-1.24.968-2.256 2.19-2.33L4 1.668h6.75z"/>
3
+ </svg>
@@ -0,0 +1,61 @@
1
+ import React, { useState, useEffect, useCallback } from 'react'
2
+ import Tippy from '@tippyjs/react'
3
+ import { copyToClipboard } from '../Helper'
4
+ import ClipboardProps from './types'
5
+ import { ReactComponent as ICCopy } from '../../Assets/Icon/ic-copy.svg'
6
+
7
+ /**
8
+ * @param content - Content to be copied
9
+ * @param copiedTippyText - Text to be shown in the tippy when the content is copied
10
+ * @param duration - Duration for which the tippy should be shown
11
+ * @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
12
+ * @param setTrigger - Callback function to set the trigger
13
+ */
14
+ export default function ClipboardButton({ content, copiedTippyText, duration, trigger, setTrigger }: ClipboardProps) {
15
+ const [copied, setCopied] = useState<boolean>(false)
16
+ const [enableTippy, setEnableTippy] = useState<boolean>(false)
17
+
18
+ const handleTextCopied = () => setCopied(true)
19
+ const handleEnableTippy = () => setEnableTippy(true)
20
+ const handleDisableTippy = () => setEnableTippy(false)
21
+ const handleCopyContent = useCallback(() => copyToClipboard(content, handleTextCopied), [content])
22
+
23
+ useEffect(() => {
24
+ if (!copied) return
25
+
26
+ const timeout = setTimeout(() => {
27
+ setCopied(false)
28
+ setTrigger(false)
29
+ }, duration)
30
+
31
+ return () => clearTimeout(timeout)
32
+ }, [copied, duration, setTrigger])
33
+
34
+ useEffect(() => {
35
+ if (trigger) {
36
+ setCopied(true)
37
+ handleCopyContent()
38
+ }
39
+ }, [trigger, handleCopyContent])
40
+
41
+ return (
42
+ <div className="icon-dim-16 flex center">
43
+ <Tippy
44
+ className="default-tt"
45
+ content={copied ? copiedTippyText : 'Copy'}
46
+ placement="right"
47
+ visible={copied || enableTippy}
48
+ >
49
+ <button
50
+ type="button"
51
+ className="dc__hover-n100 dc__outline-none-imp p-0 flex bcn-0 dc__no-border"
52
+ onMouseEnter={handleEnableTippy}
53
+ onMouseLeave={handleDisableTippy}
54
+ onClick={handleCopyContent}
55
+ >
56
+ <ICCopy className="icon-dim-16" />
57
+ </button>
58
+ </Tippy>
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import ClipboardButton from '../ClipboardButton'
4
+
5
+ jest.mock('../../helpers/Helpers', () => ({
6
+ copyToClipboard: jest.fn().mockImplementation((content, callback) => {
7
+ callback()
8
+ }),
9
+ }))
10
+
11
+ describe('When ClipboardButton mounts', () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks()
14
+ })
15
+
16
+ it('should show the copy icon', () => {
17
+ render(
18
+ <ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
19
+ )
20
+ expect(screen.getByRole('button')).toBeTruthy()
21
+ })
22
+
23
+ it('should show tippy on hover', () => {
24
+ render(
25
+ <ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
26
+ )
27
+ fireEvent.mouseEnter(screen.getByRole('button'))
28
+ expect(screen.getByText('Copy')).toBeTruthy()
29
+ })
30
+
31
+ it('should show copiedTippyText when trigger is true', () => {
32
+ render(<ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger setTrigger={null} />)
33
+ expect(screen.getByText('test')).toBeTruthy()
34
+ })
35
+
36
+ it('should call copyToClipboard when clicked', () => {
37
+ render(
38
+ <ClipboardButton content="test" copiedTippyText="test" duration={1000} trigger={false} setTrigger={null} />,
39
+ )
40
+ fireEvent.click(screen.getByRole('button'))
41
+ expect(screen.getByText('test')).toBeTruthy()
42
+ })
43
+ })
@@ -0,0 +1,7 @@
1
+ export default interface ClipboardProps {
2
+ content: string
3
+ copiedTippyText: string
4
+ duration: number
5
+ trigger: boolean
6
+ setTrigger: React.Dispatch<React.SetStateAction<boolean>>
7
+ }
@@ -1,13 +1,13 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
- import { ServerErrors } from './ServerError'
2
+ import { useLocation } from 'react-router-dom'
3
+ import moment from 'moment'
3
4
  import { toast } from 'react-toastify'
4
5
  import * as Sentry from '@sentry/browser'
6
+ import { ServerErrors } from './ServerError'
5
7
  import { toastAccessDenied } from './ToastBody'
6
8
  import { ERROR_EMPTY_SCREEN, TOKEN_COOKIE_NAME } from './Constants'
7
9
  import { ReactComponent as FormError } from '../Assets/Icon/ic-warning.svg'
8
- import moment from 'moment';
9
- import { UseSearchString } from './Types'
10
- import { useLocation } from 'react-router-dom'
10
+ import { AsyncOptions, AsyncState, UseSearchString } from './Types'
11
11
 
12
12
  toast.configure({
13
13
  autoClose: 3000,
@@ -336,17 +336,17 @@ export function CustomInput({
336
336
  }
337
337
 
338
338
  export function handleUTCTime(ts: string, isRelativeTime = false) {
339
- let timestamp = "";
340
- try {
341
- if (ts && ts.length) {
342
- let date = moment(ts);
343
- if (isRelativeTime) timestamp = date.fromNow();
344
- else timestamp = date.format("ddd DD MMM YYYY HH:mm:ss");
345
- }
346
- } catch (error) {
347
- console.error("Error Parsing Date:", ts);
348
- }
349
- return timestamp;
339
+ let timestamp = ''
340
+ try {
341
+ if (ts && ts.length) {
342
+ let date = moment(ts)
343
+ if (isRelativeTime) timestamp = date.fromNow()
344
+ else timestamp = date.format('ddd DD MMM YYYY HH:mm:ss')
345
+ }
346
+ } catch (error) {
347
+ console.error('Error Parsing Date:', ts)
348
+ }
349
+ return timestamp
350
350
  }
351
351
 
352
352
  export function useSearchString(): UseSearchString {
@@ -368,9 +368,122 @@ export function useSearchString(): UseSearchString {
368
368
  return { queryParams, searchParams }
369
369
  }
370
370
 
371
-
372
371
  export const closeOnEscKeyPressed = (e: any, actionClose: () => void) => {
373
- if (e.keyCode === 27 || e.key === 'Escape') {
372
+ if (e.keyCode === 27 || e.key === 'Escape') {
374
373
  actionClose()
375
374
  }
376
- }
375
+ }
376
+
377
+ const unsecureCopyToClipboard = (str, callback = noop) => {
378
+ const listener = function (ev) {
379
+ ev.preventDefault()
380
+ ev.clipboardData.setData('text/plain', str)
381
+ }
382
+ document.addEventListener('copy', listener)
383
+ document.execCommand('copy')
384
+ document.removeEventListener('copy', listener)
385
+ callback()
386
+ }
387
+
388
+ /**
389
+ * It will copy the passed content to clipboard and invoke the callback function, in case of error it will show the toast message.
390
+ * On HTTP system clipboard is not supported, so it will use the unsecureCopyToClipboard function
391
+ * @param str
392
+ * @param callback
393
+ */
394
+ export function copyToClipboard(str, callback = noop) {
395
+ if (!str) {
396
+ return
397
+ }
398
+
399
+ if (window.isSecureContext && navigator.clipboard) {
400
+ navigator.clipboard
401
+ .writeText(str)
402
+ .then(() => {
403
+ callback()
404
+ })
405
+ .catch(() => {
406
+ toast.error('Failed to copy to clipboard')
407
+ })
408
+ } else {
409
+ unsecureCopyToClipboard(str, callback)
410
+ }
411
+ }
412
+
413
+ export function useAsync<T>(
414
+ func: (...rest) => Promise<T>,
415
+ dependencyArray: any[] = [],
416
+ shouldRun = true,
417
+ options: AsyncOptions = { resetOnChange: true },
418
+ ): [boolean, T, any | null, () => void, React.Dispatch<any>, any[]] {
419
+ const [state, setState] = useState<AsyncState<T>>({
420
+ loading: true,
421
+ result: null,
422
+ error: null,
423
+ dependencies: dependencyArray,
424
+ })
425
+ const mounted = useRef(true)
426
+ const dependencies: any[] = useMemo(() => {
427
+ return [...dependencyArray, shouldRun]
428
+ }, [...dependencyArray, shouldRun])
429
+
430
+ const reload = () => {
431
+ async function call() {
432
+ try {
433
+ setState((state) => ({
434
+ ...state,
435
+ loading: true,
436
+ }))
437
+ const result = await func()
438
+ if (mounted.current)
439
+ setState((state) => ({
440
+ ...state,
441
+ result,
442
+ error: null,
443
+ loading: false,
444
+ }))
445
+ } catch (error: any) {
446
+ if (mounted.current)
447
+ setState((state) => ({
448
+ ...state,
449
+ error,
450
+ loading: false,
451
+ }))
452
+ }
453
+ }
454
+ call()
455
+ }
456
+
457
+ useEffect(() => {
458
+ if (!shouldRun) {
459
+ setState((state) => ({ ...state, loading: false }))
460
+ return
461
+ }
462
+ setState((state) => ({ ...state, dependencies: dependencyArray }))
463
+ reload()
464
+ return () =>
465
+ setState((state) => ({
466
+ ...state,
467
+ loading: false,
468
+ error: null,
469
+ ...(options.resetOnChange ? { result: null } : {}),
470
+ }))
471
+ }, dependencies)
472
+
473
+ useEffect(() => {
474
+ mounted.current = true
475
+ return () => {
476
+ mounted.current = false
477
+ }
478
+ }, [])
479
+
480
+ const setResult = (param) => {
481
+ if (typeof param === 'function') {
482
+ setState((state) => ({ ...state, result: param(state.result) }))
483
+ } else {
484
+ setState((state) => ({ ...state, result: param }))
485
+ }
486
+ }
487
+
488
+ return [state.loading, state.result, state.error, reload, setResult, state.dependencies]
489
+ }
@@ -395,4 +395,16 @@ export interface ResizableTextareaProps {
395
395
  disabled?: boolean
396
396
  name?: string
397
397
  dataTestId?: string
398
- }
398
+ }
399
+
400
+ export interface AsyncState<T> {
401
+ loading: boolean
402
+ result: T
403
+ error: null
404
+ dependencies: any[]
405
+ }
406
+
407
+
408
+ export interface AsyncOptions {
409
+ resetOnChange: boolean
410
+ }
@@ -37,3 +37,4 @@ export * from './ImageTags.Types'
37
37
  export * from './ResizableTextarea'
38
38
  export { default as DebouncedSearch } from './DebouncedSearch/DebouncedSearch'
39
39
  export { default as Grid } from './Grid/Grid'
40
+ export { default as ClipboardButton } from './ClipboardButton/ClipboardButton'