Skip to content

Commit c47321f

Browse files
authored
Merge pull request #2705 from devtron-labs/feat/app-clone-flow
feat: app clone flow
2 parents 61c15a3 + 39a3fe9 commit c47321f

File tree

29 files changed

+665
-348
lines changed

29 files changed

+665
-348
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"homepage": "/dashboard",
66
"dependencies": {
7-
"@devtron-labs/devtron-fe-common-lib": "1.13.0-pre-6",
7+
"@devtron-labs/devtron-fe-common-lib": "1.13.0-pre-9",
88
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
99
"@rjsf/core": "^5.13.3",
1010
"@rjsf/utils": "^5.13.3",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
Button,
3+
ComponentSizeType,
4+
DetectBottom,
5+
GenericInfoCardBorderVariant,
6+
GenericInfoCardListing,
7+
GenericInfoListSkeleton,
8+
Icon,
9+
SearchBar,
10+
useStateFilters,
11+
} from '@devtron-labs/devtron-fe-common-lib'
12+
13+
import { CreationMethodType } from '../types'
14+
import { AppCloneListProps } from './types'
15+
import { useDevtronCloneList } from './useDevtronCloneList'
16+
17+
export const AppCloneList = ({ handleCloneAppClick, isJobView, handleCreationMethodChange }: AppCloneListProps) => {
18+
const { searchKey, handleSearch, clearFilters } = useStateFilters()
19+
20+
const { isListLoading, list, listError, reloadList, loadMoreData, hasMoreData, isLoadingMore, hasError } =
21+
useDevtronCloneList({
22+
handleCloneAppClick,
23+
isJobView,
24+
searchKey,
25+
})
26+
27+
const handleLoadMore = async () => {
28+
if (isLoadingMore || !hasMoreData) return
29+
await loadMoreData()
30+
}
31+
32+
const handleCreateFromScratch = () => {
33+
handleCreationMethodChange(CreationMethodType.blank)
34+
}
35+
36+
const renderCreateFromScratchButton = () => (
37+
<Button
38+
dataTestId="create-app-modal-create-from-scratch-btn"
39+
text="Create from scratch"
40+
onClick={handleCreateFromScratch}
41+
startIcon={<Icon name="ic-new" color={null} />}
42+
/>
43+
)
44+
45+
return (
46+
<div className="flex-grow-1 flexbox-col dc__overflow-auto">
47+
<div className="flexbox-col dc__gap-12 pt-20 px-20">
48+
<h2 className="m-0 fs-15 lh-1-5 fw-6 cn-9">Choose {isJobView ? 'a job' : 'an application'} to clone</h2>
49+
50+
<SearchBar
51+
dataTestId="template-list-search"
52+
initialSearchText={searchKey}
53+
size={ComponentSizeType.medium}
54+
handleEnter={handleSearch}
55+
inputProps={{
56+
placeholder: `Search ${isJobView ? 'job' : 'application'}`,
57+
}}
58+
/>
59+
</div>
60+
<div className="flex-grow-1 flexbox-col dc__gap-12 p-20 dc__overflow-auto">
61+
<GenericInfoCardListing
62+
borderVariant={GenericInfoCardBorderVariant.ROUNDED}
63+
list={list}
64+
searchKey={searchKey}
65+
isLoading={isListLoading}
66+
error={listError}
67+
reloadList={reloadList}
68+
handleClearFilters={clearFilters}
69+
emptyStateConfig={{
70+
title: 'Nothing to Clone… Yet!',
71+
subTitle: `You haven’t created any ${isJobView ? 'job' : 'application'} to clone. Kick things off by crafting one from scratch—it’s quick and easy!`,
72+
renderButton: renderCreateFromScratchButton,
73+
}}
74+
/>
75+
{hasMoreData && isLoadingMore && <GenericInfoListSkeleton />}
76+
77+
{hasMoreData && !isLoadingMore && <DetectBottom callback={handleLoadMore} hasError={hasError} />}
78+
</div>
79+
</div>
80+
)
81+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './AppCloneList'
2+
export { useDevtronCloneList } from './useDevtronCloneList'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { BaseAppMetaData, GenericInfoCardListingProps } from '@devtron-labs/devtron-fe-common-lib'
2+
3+
import { APP_TYPE } from '@Config/constants'
4+
5+
import { SidebarProps } from '../types'
6+
7+
export interface AppCloneListProps extends Pick<SidebarProps, 'handleCreationMethodChange'> {
8+
handleCloneAppClick: ({ appId, appName }: BaseAppMetaData) => void
9+
isJobView?: boolean
10+
}
11+
12+
export type CloneListResponse = {
13+
type: APP_TYPE.DEVTRON_APPS | APP_TYPE.JOB
14+
list: GenericInfoCardListingProps['list']
15+
totalCount: number
16+
}
17+
18+
export interface CloneListTypes {
19+
offset?: number
20+
isJobView?: boolean
21+
searchKey?: string
22+
cloneListAbortControllerRef: React.MutableRefObject<AbortController>
23+
handleCloneAppClick: (app: BaseAppMetaData) => void
24+
}
25+
26+
export interface DevtronAppCloneListProps extends Pick<AppCloneListProps, 'handleCloneAppClick' | 'isJobView'> {
27+
searchKey: string
28+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2024. Devtron Inc.
3+
*/
4+
import { useEffect, useRef, useState } from 'react'
5+
6+
import { getIsRequestAborted, showError, useAsync } from '@devtron-labs/devtron-fe-common-lib'
7+
8+
import { APP_TYPE } from '@Config/constants'
9+
10+
import { fetchDevtronCloneList } from '../service'
11+
import { DevtronAppCloneListProps } from './types'
12+
13+
export const useDevtronCloneList = ({ handleCloneAppClick, isJobView, searchKey }: DevtronAppCloneListProps) => {
14+
const cloneListAbortControllerRef = useRef(new AbortController())
15+
const [isLoadingMore, setIsLoadingMore] = useState(false)
16+
const [hasError, setHasError] = useState(false)
17+
18+
useEffect(
19+
() => () => {
20+
cloneListAbortControllerRef.current.abort()
21+
},
22+
[],
23+
)
24+
25+
const [isListLoading, listResponse, listError, reloadList, setListResponse] = useAsync(() =>
26+
fetchDevtronCloneList({
27+
isJobView,
28+
searchKey,
29+
cloneListAbortControllerRef,
30+
handleCloneAppClick,
31+
}),
32+
)
33+
34+
const loadMoreData = async () => {
35+
if (isLoadingMore || !listResponse) return
36+
37+
setIsLoadingMore(true)
38+
try {
39+
if (listResponse.type === APP_TYPE.JOB) {
40+
const currentList = listResponse.list
41+
42+
const response = await fetchDevtronCloneList({
43+
isJobView,
44+
searchKey,
45+
offset: currentList.length,
46+
cloneListAbortControllerRef,
47+
handleCloneAppClick,
48+
})
49+
50+
setListResponse({
51+
...response,
52+
list: [...currentList, ...response.list],
53+
})
54+
}
55+
} catch (error) {
56+
setHasError(true)
57+
showError(error)
58+
} finally {
59+
setIsLoadingMore(false)
60+
}
61+
}
62+
63+
return {
64+
isListLoading: isListLoading || getIsRequestAborted(listError),
65+
list: listResponse?.list ?? [],
66+
listError,
67+
reloadList,
68+
totalCount: listResponse?.totalCount ?? 0,
69+
loadMoreData,
70+
hasMoreData: listResponse?.type === APP_TYPE.JOB && (listResponse.list?.length ?? 0) < listResponse.totalCount,
71+
hasError,
72+
isLoadingMore,
73+
}
74+
}

src/Pages/App/CreateAppModal/AppToCloneSelector.tsx

Lines changed: 0 additions & 80 deletions
This file was deleted.

src/Pages/App/CreateAppModal/ApplicationInfoForm.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,15 @@ import { ChangeEvent } from 'react'
1919
import { CustomInput, TagsContainer, Textarea } from '@devtron-labs/devtron-fe-common-lib'
2020

2121
import { ReactComponent as ICCaretLeftSmall } from '@Icons/ic-caret-left-small.svg'
22-
import { ReactComponent as ICDevtronApp } from '@Icons/ic-devtron-app.svg'
2322
import { importComponentFromFELibrary } from '@Components/common'
2423
import { APP_TYPE } from '@Config/constants'
24+
import { getAppIconWithBackground } from '@Config/utils'
2525

26-
import AppToCloneSelector from './AppToCloneSelector'
2726
import ProjectSelector from './ProjectSelector'
2827
import {
2928
ApplicationInfoFormProps,
3029
CreateAppFormStateActionType,
3130
CreateAppFormStateType,
32-
CreationMethodType,
3331
HandleFormStateChangeParamsType,
3432
ProjectSelectorProps,
3533
} from './types'
@@ -71,17 +69,12 @@ const ApplicationInfoForm = ({
7169
})
7270
}
7371

74-
const handleCloneIdChange = (cloneId) => {
75-
handleFormStateChange({
76-
action: CreateAppFormStateActionType.updateCloneAppId,
77-
value: cloneId,
78-
})
79-
}
72+
const isMandatoryTag = formState.tags.some((tag) => tag.data.tagKey.required)
8073

8174
return (
8275
// key is required for ensuring autoFocus on name on creation method change
8376
<div className="flexbox-col dc__gap-16 p-20 br-8 border__secondary bg__primary" key={selectedCreationMethod}>
84-
<ICDevtronApp className="icon-dim-48 dc__no-shrink" />
77+
{getAppIconWithBackground(isJobView ? APP_TYPE.JOB : APP_TYPE.DEVTRON_APPS, 48)}
8578
<div className="flexbox dc__gap-8">
8679
<ProjectSelector
8780
selectedProjectId={formState.projectId}
@@ -122,7 +115,9 @@ const ApplicationInfoForm = ({
122115
<ICCaretLeftSmall
123116
className={`scn-7 dc__no-shrink dc__transition--transform ${isTagsAccordionExpanded ? 'dc__flip-270' : 'dc__flip-180'}`}
124117
/>
125-
<span className="fs-13 fw-6 lh-20 cn-9">Add tags to {isJobView ? 'job' : 'application'}</span>
118+
<span className={`fs-13 fw-6 lh-20 cn-9 ${isMandatoryTag ? 'dc__required-field' : ''}`}>
119+
Add tags to {isJobView ? 'job' : 'application'}
120+
</span>
126121
</button>
127122
<div className={!isTagsAccordionExpanded ? 'dc__hide-section' : ''}>
128123
{MandatoryTagsContainer ? (
@@ -147,13 +142,6 @@ const ApplicationInfoForm = ({
147142
)}
148143
</div>
149144
</div>
150-
{selectedCreationMethod === CreationMethodType.clone && (
151-
<AppToCloneSelector
152-
error={formErrorState.cloneAppId}
153-
isJobView={isJobView}
154-
handleCloneIdChange={handleCloneIdChange}
155-
/>
156-
)}
157145
</div>
158146
)
159147
}

0 commit comments

Comments
 (0)