Skip to content

Commit f42dede

Browse files
committed
feat: add export functionality for Jobs and Apps with CSV headers and data types
1 parent 90ba94e commit f42dede

File tree

13 files changed

+125
-87
lines changed

13 files changed

+125
-87
lines changed

src/components/Jobs/JobList/JobListFilters.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@
1515
*/
1616

1717
import {
18+
ExportToCsv,
19+
ExportToCsvProps,
1820
FilterSelectPicker,
1921
SearchBar,
2022
SelectPickerOptionType,
2123
useGetUserRoles,
2224
} from '@devtron-labs/devtron-fe-common-lib'
2325

24-
import { FILE_NAMES } from '@Components/common/ExportToCsv/constants'
25-
import ExportToCsv from '@Components/common/ExportToCsv/ExportToCsv'
26-
2726
import { getAppListDataToExport } from '../Service'
2827
import { JobListFilterProps, JobListUrlFilters } from '../Types'
2928
import { getJobStatusLabelFromValue } from '../Utils'
29+
import { JOB_LIST_EXPORT_HEADERS } from './constants'
30+
import { ExportJobDataType } from './types'
3031

3132
const JobListFilters = ({
3233
masterFilters,
@@ -40,7 +41,8 @@ const JobListFilters = ({
4041
}: JobListFilterProps) => {
4142
const { isSuperAdmin } = useGetUserRoles()
4243
const { searchKey, status, environment, project } = filterConfig
43-
const getJobsDataToExport = async () => getAppListDataToExport(payload, searchKey, jobListCount)
44+
const getJobsDataToExport: ExportToCsvProps<keyof ExportJobDataType>['apiPromise'] = async ({ signal }) =>
45+
getAppListDataToExport(payload, searchKey, jobListCount, signal)
4446

4547
const handleUpdateFilters = (filterKey: JobListUrlFilters) => (selectedOptions: SelectPickerOptionType[]) => {
4648
updateSearchParams({ [filterKey]: selectedOptions.map((option) => String(option.value)) })
@@ -108,10 +110,11 @@ const JobListFilters = ({
108110
{isSuperAdmin && (
109111
<>
110112
<div className="dc__border-right h-16" />
111-
<ExportToCsv
113+
<ExportToCsv<keyof ExportJobDataType>
112114
apiPromise={getJobsDataToExport}
113-
fileName={FILE_NAMES.Jobs}
115+
fileName="Devtron Jobs Data"
114116
disabled={!jobListCount}
117+
headers={JOB_LIST_EXPORT_HEADERS}
115118
/>
116119
</>
117120
)}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ExportToCsvProps } from '@devtron-labs/devtron-fe-common-lib'
2+
3+
import { ExportJobDataType } from './types'
4+
5+
export const JOB_LIST_EXPORT_HEADERS: ExportToCsvProps<keyof ExportJobDataType>['headers'] = [
6+
{ label: 'Job Name', key: 'jobName' },
7+
{ label: 'Job ID', key: 'jobId' },
8+
{ label: 'Description', key: 'description' },
9+
{ label: 'Job Pipeline ID', key: 'ciPipelineId' },
10+
{ label: 'Job Pipeline Name', key: 'ciPipelineName' },
11+
{ label: 'Last Run Status', key: 'status' },
12+
{ label: 'Last Run At', key: 'lastRunAt' },
13+
{ label: 'Last Success At', key: 'lastSuccessAt' },
14+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface ExportJobDataType {
2+
jobName: string
3+
jobId: string
4+
description: string
5+
ciPipelineId: string
6+
ciPipelineName: string
7+
status: string
8+
lastRunAt: string
9+
lastSuccessAt: string
10+
}

src/components/Jobs/Service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { sortOptionsByLabel } from '../common'
2121
import { getProjectList } from '../project/service'
2222
import { JobCIPipeline, JobList, JobsMasterFilters } from './Types'
2323
import { JOB_STATUS_OPTIONS } from './Constants'
24+
import { ExportJobDataType } from './JobList/types'
2425

2526
export const getJobs = async (request, options?: APIOptions) => {
2627
const response = (await post(Routes.JOB_LIST, request, options)) as JobList
@@ -71,7 +72,8 @@ export const getAppListDataToExport = (
7172
payloadParsedFromUrl: Record<string, any>,
7273
searchString: string,
7374
jobCount: number,
74-
) => {
75+
signal: AbortSignal,
76+
): Promise<ExportJobDataType[]> => {
7577
return getJobs(
7678
typeof payloadParsedFromUrl === 'object'
7779
? {
@@ -91,6 +93,7 @@ export const getAppListDataToExport = (
9193
offset: 0,
9294
size: jobCount,
9395
},
96+
{ signal }
9497
).then(({ result }) => {
9598
if (result.jobContainers) {
9699
const _jobDataList = []

src/components/ResourceBrowser/ResourceList/ResourceFilterOptions.tsx

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,9 @@ import {
2525
GVK_FILTER_API_VERSION_QUERY_PARAM_KEY,
2626
GVK_FILTER_KIND_QUERY_PARAM_KEY,
2727
GVKOptionValueType,
28-
Icon,
2928
Nodes,
3029
noop,
3130
OptionType,
32-
ResourceRecommenderHeaderType,
3331
SearchBar,
3432
SegmentedControl,
3533
SegmentedControlProps,
@@ -41,8 +39,6 @@ import {
4139
} from '@devtron-labs/devtron-fe-common-lib'
4240

4341
import { ReactComponent as NamespaceIcon } from '@Icons/ic-env.svg'
44-
import { FILE_NAMES } from '@Components/common/ExportToCsv/constants'
45-
import ExportToCsv from '@Components/common/ExportToCsv/ExportToCsv'
4642

4743
import { convertToOptionsList, importComponentFromFELibrary } from '../../common'
4844
import { NAMESPACE_NOT_APPLICABLE_OPTION, NAMESPACE_NOT_APPLICABLE_TEXT } from '../Constants'
@@ -51,11 +47,6 @@ import { ResourceFilterOptionsProps } from '../Types'
5147
import { K8sResourceListURLParams } from './types'
5248

5349
const FilterButton = importComponentFromFELibrary('FilterButton', null, 'function')
54-
const getResourceRecommendationsCSVData = importComponentFromFELibrary(
55-
'getResourceRecommendationsCSVData',
56-
null,
57-
'function',
58-
)
5950
const ResourceRecommenderActionMenu = importComponentFromFELibrary('ResourceRecommenderActionMenu', null, 'function')
6051

6152
const ResourceFilterOptions = ({
@@ -71,7 +62,6 @@ const ResourceFilterOptions = ({
7162
updateSearchParams,
7263
filteredRows,
7364
gvkFilterConfig,
74-
isResourceListLoading,
7565
resourceRecommenderConfig,
7666
selectedAPIVersionGVKFilter,
7767
selectedKindGVKFilter,
@@ -179,9 +169,6 @@ const ResourceFilterOptions = ({
179169
)
180170
}
181171

182-
const getResourcesToExport = (): Promise<Record<ResourceRecommenderHeaderType, string>[]> =>
183-
Promise.resolve(getResourceRecommendationsCSVData(filteredRows.map((row) => row.data)))
184-
185172
const onNamespaceFilterKeyDown: SelectPickerProps['onKeyDown'] = (e) => {
186173
if (e.key === 'Escape' || e.key === 'Esc') {
187174
e.preventDefault()
@@ -299,21 +286,8 @@ const ResourceFilterOptions = ({
299286
/>
300287
</div>
301288

302-
{isResourceRecommender && ResourceRecommenderActionMenu && getResourceRecommendationsCSVData && (
303-
<ResourceRecommenderActionMenu {...resourceLastScannedOnDetails}>
304-
<ExportToCsv
305-
fileName={FILE_NAMES.ResourceRecommendations}
306-
disabled={isResourceListLoading}
307-
apiPromise={getResourcesToExport}
308-
triggerElementClassname="bg__hover dc__transparent flexbox dc__gap-8 px-8 py-6 w-100"
309-
hideExportResultModal
310-
>
311-
<span className="mt-2 flex dc__no-shrink">
312-
<Icon name="ic-download" size={16} color="N800" />
313-
</span>
314-
<span className="cn-9 fs-13 fw-4 lh-1-5">Export CSV</span>
315-
</ExportToCsv>
316-
</ResourceRecommenderActionMenu>
289+
{isResourceRecommender && ResourceRecommenderActionMenu && (
290+
<ResourceRecommenderActionMenu {...resourceLastScannedOnDetails} filteredRows={filteredRows} />
317291
)}
318292
</div>
319293
</div>

src/components/ResourceBrowser/ResourceList/ResourceRecommenderTableViewWrapper.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export const ResourceRecommenderTableViewWrapper = ({
2626
searchKey,
2727
handleSearch,
2828
filteredRows,
29-
isResourceListLoading,
3029
selectedResource,
3130
gvkFilterConfig,
3231
updateSearchParams,
@@ -42,7 +41,6 @@ export const ResourceRecommenderTableViewWrapper = ({
4241
setSearchText={handleSearch}
4342
searchPlaceholder="Search"
4443
filteredRows={filteredRows}
45-
isResourceListLoading={isResourceListLoading}
4644
selectedResource={selectedResource}
4745
gvkFilterConfig={gvkFilterConfig}
4846
updateSearchParams={updateSearchParams}

src/components/ResourceBrowser/ResourceList/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export type DynamicTabComponentWrapperProps = Pick<
168168

169169
export interface ResourceRecommenderTableViewWrapperProps
170170
extends TableViewWrapperProps<
171-
unknown,
171+
K8sResourceDetailDataType,
172172
FiltersTypeEnum.URL,
173173
ResourceFilterOptionsProps & {
174174
resourceListError: ServerErrors

src/components/ResourceBrowser/Types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ export interface ClusterOptionType extends OptionType {
9292
}
9393

9494
export interface ResourceFilterOptionsProps
95-
extends Pick<TableViewWrapperProps<unknown, FiltersTypeEnum.URL>, 'updateSearchParams' | 'filteredRows'>,
95+
extends Pick<
96+
TableViewWrapperProps<K8sResourceDetailDataType, FiltersTypeEnum.URL>,
97+
'updateSearchParams' | 'filteredRows'
98+
>,
9699
Partial<Pick<K8sResourceListFilterType, 'eventType'>> {
97100
selectedResource: ApiResourceGroupType
98101
selectedCluster?: ClusterOptionType
@@ -107,7 +110,6 @@ export interface ResourceFilterOptionsProps
107110
* @default undefined
108111
*/
109112
searchPlaceholder?: string
110-
isResourceListLoading?: boolean
111113
gvkFilterConfig?: {
112114
gvkOptions: GroupBase<SelectPickerOptionType<GVKOptionValueType>>[]
113115
areGVKOptionsLoading: boolean

src/components/app/list-new/AppListFilters.tsx

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import ReactGA from 'react-ga4'
2020
import {
2121
AppListConstants,
2222
ComponentSizeType,
23+
ExportToCsv,
24+
ExportToCsvProps,
2325
FilterSelectPicker,
2426
GroupedFilterSelectPicker,
2527
GroupedFilterSelectPickerProps,
@@ -30,12 +32,14 @@ import {
3032
useGetUserRoles,
3133
} from '@devtron-labs/devtron-fe-common-lib'
3234

33-
import { FILE_NAMES } from '@Components/common/ExportToCsv/constants'
34-
import ExportToCsv from '@Components/common/ExportToCsv/ExportToCsv'
35-
3635
import { getDevtronAppListDataToExport } from './AppListService'
3736
import { AppListFiltersProps, AppListUrlFilters, AppStatuses } from './AppListType'
38-
import { APP_STATUS_FILTER_OPTIONS, SELECT_CLUSTER_TIPPY, TEMPLATE_TYPE_FILTER_OPTIONS } from './Constants'
37+
import {
38+
APP_STATUS_FILTER_OPTIONS,
39+
APPLIST_EXPORT_HEADERS,
40+
SELECT_CLUSTER_TIPPY,
41+
TEMPLATE_TYPE_FILTER_OPTIONS,
42+
} from './Constants'
3943
import { getAppListFilters, getAppTabNameFromAppType, useFilterOptions } from './list.utils'
4044

4145
const AppListFilters = ({
@@ -203,6 +207,34 @@ const AppListFilters = ({
203207
},
204208
}
205209

210+
const getExportToCsvApiPromise: ExportToCsvProps<(typeof APPLIST_EXPORT_HEADERS)[number]['key']>['apiPromise'] = ({
211+
signal,
212+
}) =>
213+
getDevtronAppListDataToExport(
214+
filterConfig,
215+
appListFiltersResponse?.appListFilters.result.environments,
216+
namespaceListResponse?.result,
217+
appListFiltersResponse?.appListFilters.result.clusters,
218+
appListFiltersResponse?.appListFilters.result.teams,
219+
appCount,
220+
signal,
221+
)
222+
223+
const renderExportToCSV = () => {
224+
if (!showExportCsvButton) {
225+
return null
226+
}
227+
228+
return (
229+
<ExportToCsv<(typeof APPLIST_EXPORT_HEADERS)[number]['key']>
230+
headers={APPLIST_EXPORT_HEADERS}
231+
apiPromise={getExportToCsvApiPromise}
232+
fileName="Devtron Apps"
233+
disabled={!appCount}
234+
/>
235+
)
236+
}
237+
206238
return (
207239
<div className="search-filter-section">
208240
<div className="flex left dc__gap-8">
@@ -251,22 +283,7 @@ const AppListFilters = ({
251283
)}
252284
</div>
253285

254-
{window._env_.FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE && showExportCsvButton && (
255-
<ExportToCsv
256-
apiPromise={() =>
257-
getDevtronAppListDataToExport(
258-
filterConfig,
259-
appListFiltersResponse?.appListFilters.result.environments,
260-
namespaceListResponse?.result,
261-
appListFiltersResponse?.appListFilters.result.clusters,
262-
appListFiltersResponse?.appListFilters.result.teams,
263-
appCount,
264-
)
265-
}
266-
fileName={FILE_NAMES.Apps}
267-
disabled={!appCount}
268-
/>
269-
)}
286+
{window._env_.FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE && renderExportToCSV()}
270287

271288
{!window._env_.FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE && (
272289
<div className="flexbox dc__gap-8 dc__align-items-center">
@@ -384,20 +401,7 @@ const AppListFilters = ({
384401
{showExportCsvButton && (
385402
<>
386403
<div className="dc__border-right h-16" />
387-
<ExportToCsv
388-
apiPromise={() =>
389-
getDevtronAppListDataToExport(
390-
filterConfig,
391-
appListFiltersResponse?.appListFilters.result.environments,
392-
namespaceListResponse?.result,
393-
appListFiltersResponse?.appListFilters.result.clusters,
394-
appListFiltersResponse?.appListFilters.result.teams,
395-
appCount,
396-
)
397-
}
398-
fileName={FILE_NAMES.Apps}
399-
disabled={!appCount}
400-
/>
404+
{renderExportToCSV()}
401405
</>
402406
)}
403407
</div>

src/components/app/list-new/AppListService.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { Moment12HourFormat, Routes } from '../../../config'
3030
import {
3131
AppListFilterConfig,
3232
AppListPayloadType,
33+
ExportAppListDataType,
3334
FluxCDTemplateType,
3435
GenericAppListResponse,
3536
GenericAppType,
@@ -67,17 +68,18 @@ export const getDevtronAppListDataToExport = (
6768
clusterList: Cluster[],
6869
projectList: Teams[],
6970
appCount: number,
70-
) => {
71+
signal: AbortSignal,
72+
): Promise<ExportAppListDataType[]> => {
7173
const appListPayload: AppListPayloadType = {
7274
...getDevtronAppListPayload(filterConfig, environmentList, namespaceList),
7375
offset: 0,
7476
size: appCount,
7577
} // Over riding size and offset as we need all list (no pagination)
7678
const clusterMap = new Map<string, number>()
7779
clusterList.forEach((cluster) => clusterMap.set(cluster.cluster_name, cluster.id))
78-
return getAppList(appListPayload).then(({ result }) => {
80+
return getAppList(appListPayload, { signal }).then(({ result }) => {
7981
if (result.appContainers) {
80-
const _appDataList = []
82+
const _appDataList: ExportAppListDataType[] = []
8183
for (const _app of result.appContainers) {
8284
if (_app.environments) {
8385
for (const _env of _app.environments) {
@@ -104,7 +106,7 @@ export const getDevtronAppListDataToExport = (
104106
appId: _app.appId,
105107
appName: _app.appName,
106108
projectId: _app.projectId,
107-
projectName: projectList.find((project) => project.id === +_app.projectId) || '-',
109+
projectName: projectList.find((project) => project.id === +_app.projectId)?.name || '-',
108110
environmentId: '-',
109111
environmentName: '-',
110112
clusterId: '-',

0 commit comments

Comments
 (0)