Skip to content

Commit c242cd3

Browse files
working landscapers
1 parent 65cd407 commit c242cd3

File tree

6 files changed

+275
-212
lines changed

6 files changed

+275
-212
lines changed

public/locales/en.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,16 @@
155155
"ToastContext": {
156156
"errorMessage": "useToast must be used within a ToastProvider"
157157
},
158+
"Landscapers": {
159+
"headerLandscapers": "Landscapers",
160+
"multiComboBoxPlaceholder": "Select namespace",
161+
"noItemsFound": "No Deploy items found",
162+
"deployItems": "Deploy Items",
163+
"treeDeployItem": "Deploy Item",
164+
"treeInstallation": "Installation",
165+
"treeExecution": "Execution",
166+
"noExecutionFound": "No Exeuctions found"
167+
},
158168
"CopyButton": {
159169
"copiedMessage": "Copied To Clipboard",
160170
"failedMessage": "Failed to copy"
Lines changed: 136 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,209 +1,179 @@
11
import { useTranslation } from 'react-i18next';
22
import {
3-
AnalyticalTable,
4-
AnalyticalTableColumnDefinition,
5-
AnalyticalTableScaleWidthMode,
6-
Title,
73
MultiComboBox,
4+
MultiComboBoxDomRef,
85
MultiComboBoxItem,
6+
Tree,
7+
TreeItem,
8+
Ui5CustomEvent,
99
} 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';
1311
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';
1930

2031
export function Landscapers() {
2132
const { t } = useTranslation();
22-
const apiConfig = useContext(ApiConfigContext);
2333

2434
const { data: namespaces } = useResource(ListNamespaces, {
2535
refreshInterval: resourcesInterval,
2636
});
2737

2838
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);
3339

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+
) => {
3561
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+
3766
setSelectedNamespaces(selectedValues);
3867
};
3968

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 '⚪';
13171

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,
13699
);
137100

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[]) || [];
152111

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))
162121
) : (
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+
</>
164148
)}
165-
</div>
149+
</TreeItem>
166150
);
167151
};
168152

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+
169163
return (
170164
<>
171-
<Title level="H4">{t('Providers.headerProviders')}</Title>
172-
173165
{namespaces && (
174166
<MultiComboBox
175-
placeholder={t('Select namespace')}
176-
style={{ marginBottom: '1rem', maxWidth: '400px' }}
167+
placeholder={t('Landscapers.multiComboBoxPlaceholder')}
168+
style={{ marginBottom: '1rem' }}
177169
onSelectionChange={handleSelectionChange}
178170
>
179171
{namespaces.map((ns) => (
180172
<MultiComboBoxItem key={ns.metadata.name} text={ns.metadata.name} />
181173
))}
182174
</MultiComboBox>
183175
)}
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>
207177
</>
208178
);
209179
}

0 commit comments

Comments
 (0)