@devtron-labs/devtron-fe-common-lib 0.0.30-beta1 → 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-beta1",
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,
@@ -326,7 +326,7 @@ export function CustomInput({
326
326
  tabIndex={tabIndex}
327
327
  />
328
328
  {handleError(error).map((err) => (
329
- <div className="form__error" key={err}>
329
+ <div className="form__error">
330
330
  <FormError className="form__icon form__icon--error" />
331
331
  {err}
332
332
  </div>
@@ -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
+ }
@@ -27,7 +27,6 @@ export interface ImageTaggingContainerType {
27
27
  setTagsEditable?: (tagsEditable: boolean) => void
28
28
  toggleCardMode?: (id: number) => void
29
29
  hideHardDelete?: boolean
30
- isSuperAdmin?: boolean
31
30
  }
32
31
 
33
32
  export interface ImageButtonType {
@@ -32,7 +32,6 @@ export const ImageTagsContainer = ({
32
32
  toggleCardMode,
33
33
  hideHardDelete,
34
34
  forceReInit,
35
- isSuperAdmin
36
35
  }: ImageTaggingContainerType) => {
37
36
  const [initialTags, setInitialTags] = useState<ReleaseTag[]>(imageReleaseTags ? imageReleaseTags : [])
38
37
  const [initialDescription, setInitialDescription] = useState(imageComment ? imageComment.comment : '')
@@ -45,9 +44,13 @@ export const ImageTagsContainer = ({
45
44
  const [createTags, setCreateTags] = useState<ReleaseTag[]>([])
46
45
  const [softDeleteTags, setSoftDeleteTags] = useState<ReleaseTag[]>([])
47
46
  const [hardDeleteTags, setHardDeleteTags] = useState<ReleaseTag[]>([])
47
+ const [isSuperAdmin, setSuperAdmin] = useState<boolean>(false)
48
48
  const [descriptionValidationMessage, setDescriptionValidationMessage] = useState<string>('')
49
49
  const [textInput, setTextInput] = useState<string>('')
50
50
 
51
+ useEffect(() => {
52
+ initialise()
53
+ }, [])
51
54
 
52
55
  useEffect(() => {
53
56
  reInitState()
@@ -57,6 +60,16 @@ export const ImageTagsContainer = ({
57
60
  setExistingTags(appReleaseTagNames ? appReleaseTagNames : [])
58
61
  }, [appReleaseTagNames])
59
62
 
63
+ async function initialise() {
64
+ try {
65
+ const userRole = await getUserRole()
66
+
67
+ const superAdmin = userRole?.result?.roles?.includes('role:super-admin___')
68
+ setSuperAdmin(superAdmin)
69
+ } catch (err) {
70
+ showError(err)
71
+ }
72
+ }
60
73
 
61
74
  const reInitState = () => {
62
75
  if (forceReInit || !isEditing) {
@@ -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'