@devtron-labs/devtron-fe-common-lib 0.0.30-beta1 → 0.0.31-beta1
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/dist/Common/ClipboardButton/ClipboardButton.d.ts +10 -0
- package/dist/Common/ClipboardButton/types.d.ts +8 -0
- package/dist/Common/CustomTagSelector/ResizableTagTextArea.d.ts +1 -1
- package/dist/Common/CustomTagSelector/Types.d.ts +1 -0
- package/dist/Common/Helper.d.ts +9 -1
- package/dist/Common/ImageTags.Types.d.ts +0 -1
- package/dist/Common/ImageTags.d.ts +1 -1
- package/dist/Common/Types.d.ts +9 -0
- package/dist/Common/index.d.ts +1 -0
- package/dist/index.js +6755 -6567
- package/package.json +1 -1
- package/src/Assets/Icon/ic-copy.svg +3 -0
- package/src/Common/ClipboardButton/ClipboardButton.tsx +61 -0
- package/src/Common/ClipboardButton/__tests__/ClipboardButton.test.tsx +43 -0
- package/src/Common/ClipboardButton/types.ts +7 -0
- package/src/Common/CustomTagSelector/ResizableTagTextArea.tsx +14 -6
- package/src/Common/CustomTagSelector/Types.ts +1 -0
- package/src/Common/Helper.tsx +132 -19
- package/src/Common/ImageTags.Types.ts +0 -1
- package/src/Common/ImageTags.tsx +14 -1
- package/src/Common/Types.ts +13 -1
- package/src/Common/index.ts +1 -0
package/package.json
CHANGED
|
@@ -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
|
+
})
|
|
@@ -15,6 +15,7 @@ export function ResizableTagTextArea({
|
|
|
15
15
|
refVar,
|
|
16
16
|
dependentRef,
|
|
17
17
|
dataTestId,
|
|
18
|
+
handleKeyDown,
|
|
18
19
|
}: ResizableTagTextAreaProps) {
|
|
19
20
|
const [text, setText] = useState('')
|
|
20
21
|
|
|
@@ -29,10 +30,12 @@ export function ResizableTagTextArea({
|
|
|
29
30
|
|
|
30
31
|
const reInitHeight = () => {
|
|
31
32
|
if (document.activeElement !== refVar.current) return
|
|
32
|
-
refVar.current.style.height = minHeight+ 'px'
|
|
33
|
-
dependentRef
|
|
33
|
+
refVar.current.style.height = minHeight + 'px'
|
|
34
|
+
if (dependentRef) {
|
|
35
|
+
dependentRef.current.style.height = minHeight + 'px'
|
|
36
|
+
}
|
|
34
37
|
let nextHeight = refVar.current.scrollHeight
|
|
35
|
-
if (nextHeight < dependentRef.current.scrollHeight) {
|
|
38
|
+
if (dependentRef && nextHeight < dependentRef.current.scrollHeight) {
|
|
36
39
|
nextHeight = dependentRef.current.scrollHeight
|
|
37
40
|
}
|
|
38
41
|
if (minHeight && nextHeight < minHeight) {
|
|
@@ -42,14 +45,18 @@ export function ResizableTagTextArea({
|
|
|
42
45
|
nextHeight = maxHeight
|
|
43
46
|
}
|
|
44
47
|
refVar.current.style.height = nextHeight + 'px'
|
|
45
|
-
dependentRef
|
|
48
|
+
if (dependentRef) {
|
|
49
|
+
dependentRef.current.style.height = nextHeight + 'px'
|
|
50
|
+
}
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
useThrottledEffect(reInitHeight, 500, [text])
|
|
49
54
|
|
|
50
55
|
const handleOnBlur = (event) => {
|
|
51
|
-
refVar.current.style.height = minHeight+ 'px'
|
|
52
|
-
dependentRef
|
|
56
|
+
refVar.current.style.height = minHeight + 'px'
|
|
57
|
+
if (dependentRef) {
|
|
58
|
+
dependentRef.current.style.height = minHeight + 'px'
|
|
59
|
+
}
|
|
53
60
|
onBlur && onBlur(event)
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -71,6 +78,7 @@ export function ResizableTagTextArea({
|
|
|
71
78
|
onFocus={handleOnFocus}
|
|
72
79
|
tabIndex={tabIndex}
|
|
73
80
|
data-testid={dataTestId}
|
|
81
|
+
onKeyDown={handleKeyDown}
|
|
74
82
|
/>
|
|
75
83
|
)
|
|
76
84
|
}
|
package/src/Common/Helper.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import {
|
|
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
|
|
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"
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|
package/src/Common/ImageTags.tsx
CHANGED
|
@@ -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) {
|
package/src/Common/Types.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/Common/index.ts
CHANGED
|
@@ -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'
|