|
1 | 1 | import { useTranslation } from 'react-i18next'; |
2 | 2 | import { |
3 | | - AnalyticalTable, |
4 | | - AnalyticalTableColumnDefinition, |
5 | | - AnalyticalTableScaleWidthMode, |
6 | | - Title, |
7 | 3 | MultiComboBox, |
| 4 | + MultiComboBoxDomRef, |
8 | 5 | MultiComboBoxItem, |
| 6 | + Tree, |
| 7 | + TreeItem, |
| 8 | + Ui5CustomEvent, |
9 | 9 | } from '@ui5/webcomponents-react'; |
10 | | -import useResource from '../../lib/api/useApiResource'; |
11 | | -import { ListNamespaces } from '../../lib/api/types/k8s/listNamespaces'; |
12 | | -import { useEffect, useState, useContext } from 'react'; |
| 10 | +import { useState, JSX } from 'react'; |
13 | 11 | import { resourcesInterval } from '../../lib/shared/constants'; |
14 | | -import { InstalationsRequest } from '../../lib/api/types/landscaper/listInstallations'; |
15 | | -import { ExecutionsRequest } from '../../lib/api/types/landscaper/listExecutions'; |
16 | | -import { DeployItemsRequest } from '../../lib/api/types/landscaper/listDeployItems'; |
17 | | -import { ApiConfigContext } from '../../components/Shared/k8s'; |
18 | | -import { fetchApiServerJson } from '../../lib/api/fetch'; |
| 12 | +import useResource, { |
| 13 | + useMultipleApiResources, |
| 14 | +} from '../../lib/api/useApiResource'; |
| 15 | +import { ListNamespaces } from '../../lib/api/types/k8s/listNamespaces'; |
| 16 | +import { |
| 17 | + Installation, |
| 18 | + InstalationsRequest, |
| 19 | +} from '../../lib/api/types/landscaper/listInstallations'; |
| 20 | +import { |
| 21 | + Execution, |
| 22 | + ExecutionsRequest, |
| 23 | +} from '../../lib/api/types/landscaper/listExecutions'; |
| 24 | +import { |
| 25 | + DeployItem, |
| 26 | + DeployItemsRequest, |
| 27 | +} from '../../lib/api/types/landscaper/listDeployItems'; |
| 28 | + |
| 29 | +import { MultiComboBoxSelectionChangeEventDetail } from '@ui5/webcomponents/dist/MultiComboBox.js'; |
19 | 30 |
|
20 | 31 | export function Landscapers() { |
21 | 32 | const { t } = useTranslation(); |
22 | | - const apiConfig = useContext(ApiConfigContext); |
23 | 33 |
|
24 | 34 | const { data: namespaces } = useResource(ListNamespaces, { |
25 | 35 | refreshInterval: resourcesInterval, |
26 | 36 | }); |
27 | 37 |
|
28 | 38 | const [selectedNamespaces, setSelectedNamespaces] = useState<string[]>([]); |
29 | | - const [installations, setInstallations] = useState<any[]>([]); |
30 | | - const [executions, setExecutions] = useState<any[]>([]); |
31 | | - const [deployItems, setDeployItems] = useState<any[]>([]); |
32 | | - const [loading, setLoading] = useState(false); |
33 | 39 |
|
34 | | - const handleSelectionChange = (e: CustomEvent) => { |
| 40 | + const { data: installations = [] } = useMultipleApiResources<Installation>( |
| 41 | + selectedNamespaces, |
| 42 | + InstalationsRequest, |
| 43 | + ); |
| 44 | + |
| 45 | + const { data: executions = [] } = useMultipleApiResources<Execution>( |
| 46 | + selectedNamespaces, |
| 47 | + ExecutionsRequest, |
| 48 | + ); |
| 49 | + |
| 50 | + const { data: deployItems = [] } = useMultipleApiResources<DeployItem>( |
| 51 | + selectedNamespaces, |
| 52 | + DeployItemsRequest, |
| 53 | + ); |
| 54 | + |
| 55 | + const handleSelectionChange = ( |
| 56 | + e: Ui5CustomEvent< |
| 57 | + MultiComboBoxDomRef, |
| 58 | + MultiComboBoxSelectionChangeEventDetail |
| 59 | + >, |
| 60 | + ) => { |
35 | 61 | const selectedItems = Array.from(e.detail.items || []); |
36 | | - const selectedValues = selectedItems.map((item: any) => item.text); |
| 62 | + const selectedValues = selectedItems |
| 63 | + .map((item) => item.text) |
| 64 | + .filter((text): text is string => typeof text === 'string'); |
| 65 | + |
37 | 66 | setSelectedNamespaces(selectedValues); |
38 | 67 | }; |
39 | 68 |
|
40 | | - useEffect(() => { |
41 | | - const fetchAllResources = async () => { |
42 | | - if (selectedNamespaces.length === 0) { |
43 | | - setInstallations([]); |
44 | | - setExecutions([]); |
45 | | - setDeployItems([]); |
46 | | - return; |
47 | | - } |
48 | | - |
49 | | - setLoading(true); |
50 | | - |
51 | | - try { |
52 | | - // === INSTALLATIONS === |
53 | | - const installationPaths = selectedNamespaces |
54 | | - .map((ns) => InstalationsRequest(ns).path) |
55 | | - .filter((p): p is string => p !== null && p !== undefined); |
56 | | - |
57 | | - const installationResponses = await Promise.all( |
58 | | - installationPaths.map((path) => fetchApiServerJson(path, apiConfig)), |
59 | | - ); |
60 | | - |
61 | | - const installationsData = installationResponses.flatMap( |
62 | | - (res) => res.items || [], |
63 | | - ); |
64 | | - setInstallations(installationsData); |
65 | | - |
66 | | - // === EXECUTIONS === |
67 | | - const executionPaths = selectedNamespaces |
68 | | - .map((ns) => ExecutionsRequest(ns).path) |
69 | | - .filter((p): p is string => p !== null && p !== undefined); |
70 | | - |
71 | | - const executionResponses = await Promise.all( |
72 | | - executionPaths.map((path) => fetchApiServerJson(path, apiConfig)), |
73 | | - ); |
74 | | - |
75 | | - const executionsData = executionResponses.flatMap( |
76 | | - (res) => res.items || [], |
77 | | - ); |
78 | | - setExecutions(executionsData); |
79 | | - |
80 | | - // === DEPLOY ITEMS === |
81 | | - const deployPaths = selectedNamespaces |
82 | | - .map((ns) => DeployItemsRequest(ns).path) |
83 | | - .filter((p): p is string => p !== null && p !== undefined); |
84 | | - |
85 | | - const deployResponses = await Promise.all( |
86 | | - deployPaths.map((path) => fetchApiServerJson(path, apiConfig)), |
87 | | - ); |
88 | | - |
89 | | - const deployItemsData = deployResponses.flatMap((res) => res.items || []); |
90 | | - setDeployItems(deployItemsData); |
91 | | - } catch (error) { |
92 | | - console.error(error); |
93 | | - setInstallations([]); |
94 | | - setExecutions([]); |
95 | | - setDeployItems([]); |
96 | | - } finally { |
97 | | - setLoading(false); |
98 | | - } |
99 | | - }; |
100 | | - |
101 | | - fetchAllResources(); |
102 | | - }, [selectedNamespaces, apiConfig]); |
103 | | - |
104 | | - const columns: AnalyticalTableColumnDefinition[] = [ |
105 | | - { |
106 | | - Header: t('Namespace'), |
107 | | - accessor: 'metadata.namespace', |
108 | | - }, |
109 | | - { |
110 | | - Header: t('Name'), |
111 | | - accessor: 'metadata.name', |
112 | | - }, |
113 | | - { |
114 | | - Header: t('Phase'), |
115 | | - accessor: 'status.phase', |
116 | | - }, |
117 | | - { |
118 | | - Header: t('Created At'), |
119 | | - accessor: 'metadata.creationTimestamp', |
120 | | - }, |
121 | | - ]; |
122 | | - |
123 | | - const renderRowSubComponent = (row: any) => { |
124 | | - const installation = row.original; |
125 | | - |
126 | | - const relatedExecutions = executions.filter((execution) => |
127 | | - execution.metadata.ownerReferences?.some( |
128 | | - (ref) => ref.uid === installation.metadata.uid, |
129 | | - ), |
130 | | - ); |
| 69 | + const getStatusSymbol = (phase?: string) => { |
| 70 | + if (!phase) return '⚪'; |
131 | 71 |
|
132 | | - const relatedDeployItems = deployItems.filter((deploy) => |
133 | | - deploy.metadata.ownerReferences?.some( |
134 | | - (ref) => ref.uid === installation.metadata.uid, |
135 | | - ), |
| 72 | + const phaseLower = phase.toLowerCase(); |
| 73 | + |
| 74 | + if (phaseLower === 'succeeded') { |
| 75 | + return '✅'; |
| 76 | + } else if (phaseLower === 'failed') { |
| 77 | + return '❌'; |
| 78 | + } |
| 79 | + |
| 80 | + return '⚪'; |
| 81 | + }; |
| 82 | + |
| 83 | + const renderTreeItems = (installation: Installation): JSX.Element => { |
| 84 | + const subInstallations = |
| 85 | + (installation.status?.subInstCache?.activeSubs |
| 86 | + ?.map((sub) => |
| 87 | + installations.find( |
| 88 | + (i) => |
| 89 | + i.metadata.name === sub.objectName && |
| 90 | + i.metadata.namespace === installation.metadata.namespace, |
| 91 | + ), |
| 92 | + ) |
| 93 | + .filter(Boolean) as Installation[]) || []; |
| 94 | + |
| 95 | + const execution = executions.find( |
| 96 | + (e) => |
| 97 | + e.metadata.name === installation.status?.executionRef?.name && |
| 98 | + e.metadata.namespace === installation.status?.executionRef?.namespace, |
136 | 99 | ); |
137 | 100 |
|
138 | | - return ( |
139 | | - <div style={{ padding: '10px', backgroundColor: '#f4f4f4' }}> |
140 | | - <h5>{t('Executions')}</h5> |
141 | | - {relatedExecutions.length > 0 ? ( |
142 | | - <ul> |
143 | | - {relatedExecutions.map((execution: any) => ( |
144 | | - <li key={execution.metadata.uid}> |
145 | | - {execution.metadata.name} – {execution.status.phase} |
146 | | - </li> |
147 | | - ))} |
148 | | - </ul> |
149 | | - ) : ( |
150 | | - <p>{t('No executions found')}</p> |
151 | | - )} |
| 101 | + const relatedDeployItems = |
| 102 | + (execution?.status?.deployItemCache?.activeDIs |
| 103 | + ?.map((di) => |
| 104 | + deployItems.find( |
| 105 | + (item) => |
| 106 | + item.metadata.name === di.objectName && |
| 107 | + item.metadata.namespace === execution.metadata.namespace, |
| 108 | + ), |
| 109 | + ) |
| 110 | + .filter(Boolean) as DeployItem[]) || []; |
152 | 111 |
|
153 | | - <h5 style={{ marginTop: '1rem' }}>{t('Deploy Items')}</h5> |
154 | | - {relatedDeployItems.length > 0 ? ( |
155 | | - <ul> |
156 | | - {relatedDeployItems.map((deploy: any) => ( |
157 | | - <li key={deploy.metadata.uid}> |
158 | | - {deploy.metadata.name} – {deploy.status.phase} |
159 | | - </li> |
160 | | - ))} |
161 | | - </ul> |
| 112 | + return ( |
| 113 | + <TreeItem |
| 114 | + key={installation.metadata.uid} |
| 115 | + text={`${getStatusSymbol(installation.status?.phase)} ${t( |
| 116 | + 'Landscapers.treeInstallation', |
| 117 | + )}: ${installation.metadata.name} (${installation.status?.phase || '-'})`} |
| 118 | + > |
| 119 | + {subInstallations.length > 0 ? ( |
| 120 | + subInstallations.map((sub) => renderTreeItems(sub)) |
162 | 121 | ) : ( |
163 | | - <p>{t('No deploy items found')}</p> |
| 122 | + <> |
| 123 | + <TreeItem |
| 124 | + text={`${ |
| 125 | + execution |
| 126 | + ? `${getStatusSymbol(execution.status?.phase)} ${t( |
| 127 | + 'Landscapers.treeExecution', |
| 128 | + )}: ${execution.metadata.name} (${execution.status?.phase || '-'})` |
| 129 | + : t('Landscapers.noExecutionFound') |
| 130 | + }`} |
| 131 | + /> |
| 132 | + |
| 133 | + <TreeItem text={t('Landscapers.deployItems')}> |
| 134 | + {relatedDeployItems.length > 0 ? ( |
| 135 | + relatedDeployItems.map((di) => ( |
| 136 | + <TreeItem |
| 137 | + key={di.metadata.uid} |
| 138 | + text={`${getStatusSymbol(di.status?.phase)} ${t( |
| 139 | + 'Landscapers.treeDeployItem', |
| 140 | + )}: ${di.metadata.name} (${di.status?.phase || '-'})`} |
| 141 | + /> |
| 142 | + )) |
| 143 | + ) : ( |
| 144 | + <TreeItem text={t('Landscapers.noItemsFound')} /> |
| 145 | + )} |
| 146 | + </TreeItem> |
| 147 | + </> |
164 | 148 | )} |
165 | | - </div> |
| 149 | + </TreeItem> |
166 | 150 | ); |
167 | 151 | }; |
168 | 152 |
|
| 153 | + const rootInstallations = installations.filter((inst) => { |
| 154 | + return !installations.some((parent) => |
| 155 | + parent.status?.subInstCache?.activeSubs?.some( |
| 156 | + (sub: { objectName: string }) => |
| 157 | + sub.objectName === inst.metadata.name && |
| 158 | + parent.metadata.namespace === inst.metadata.namespace, |
| 159 | + ), |
| 160 | + ); |
| 161 | + }); |
| 162 | + |
169 | 163 | return ( |
170 | 164 | <> |
171 | | - <Title level="H4">{t('Providers.headerProviders')}</Title> |
172 | | - |
173 | 165 | {namespaces && ( |
174 | 166 | <MultiComboBox |
175 | | - placeholder={t('Select namespace')} |
176 | | - style={{ marginBottom: '1rem', maxWidth: '400px' }} |
| 167 | + placeholder={t('Landscapers.multiComboBoxPlaceholder')} |
| 168 | + style={{ marginBottom: '1rem' }} |
177 | 169 | onSelectionChange={handleSelectionChange} |
178 | 170 | > |
179 | 171 | {namespaces.map((ns) => ( |
180 | 172 | <MultiComboBoxItem key={ns.metadata.name} text={ns.metadata.name} /> |
181 | 173 | ))} |
182 | 174 | </MultiComboBox> |
183 | 175 | )} |
184 | | - |
185 | | - <AnalyticalTable |
186 | | - columns={columns} |
187 | | - data={installations} |
188 | | - minRows={1} |
189 | | - loading={loading} |
190 | | - scaleWidthMode={AnalyticalTableScaleWidthMode.Smart} |
191 | | - filterable |
192 | | - retainColumnWidth |
193 | | - renderRowSubComponent={renderRowSubComponent} |
194 | | - subComponentsBehavior="IncludeHeightExpandable" |
195 | | - reactTableOptions={{ |
196 | | - autoResetHiddenColumns: false, |
197 | | - autoResetPage: false, |
198 | | - autoResetExpanded: false, |
199 | | - autoResetGroupBy: false, |
200 | | - autoResetSelectedRows: false, |
201 | | - autoResetSortBy: false, |
202 | | - autoResetFilters: false, |
203 | | - autoResetRowState: false, |
204 | | - autoResetResize: false, |
205 | | - }} |
206 | | - /> |
| 176 | + <Tree>{rootInstallations.map((inst) => renderTreeItems(inst))}</Tree> |
207 | 177 | </> |
208 | 178 | ); |
209 | 179 | } |
0 commit comments