Skip to content

Commit d754a22

Browse files
Merge branch 'main' into vitest
2 parents efe2976 + f4ae62f commit d754a22

File tree

11 files changed

+457
-185
lines changed

11 files changed

+457
-185
lines changed

package-lock.json

Lines changed: 122 additions & 85 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/locales/en.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,22 @@
149149
"accessError": "Managed Control Plane does not have access information yet",
150150
"componentsTitle": "Components",
151151
"crossplaneTitle": "Crossplane",
152-
"gitOpsTitle": "GitOps"
152+
"gitOpsTitle": "GitOps",
153+
"landscapersTitle": "Landscapers"
153154
},
154155
"ToastContext": {
155156
"errorMessage": "useToast must be used within a ToastProvider"
156157
},
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 Executions found"
167+
},
157168
"CopyButton": {
158169
"copiedMessage": "Copied To Clipboard",
159170
"failedMessage": "Failed to copy"
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { useTranslation } from 'react-i18next';
2+
import {
3+
MultiComboBox,
4+
MultiComboBoxDomRef,
5+
MultiComboBoxItem,
6+
Tree,
7+
TreeItem,
8+
Ui5CustomEvent,
9+
} from '@ui5/webcomponents-react';
10+
import { useState, JSX } from 'react';
11+
import { resourcesInterval } from '../../lib/shared/constants';
12+
import useResource, {
13+
useMultipleApiResources,
14+
} from '../../lib/api/useApiResource';
15+
import { ListNamespaces } from '../../lib/api/types/k8s/listNamespaces';
16+
import {
17+
Installation,
18+
InstallationsRequest,
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';
30+
31+
export function Landscapers() {
32+
const { t } = useTranslation();
33+
34+
const { data: namespaces } = useResource(ListNamespaces, {
35+
refreshInterval: resourcesInterval,
36+
});
37+
38+
const [selectedNamespaces, setSelectedNamespaces] = useState<string[]>([]);
39+
40+
const { data: installations = [] } = useMultipleApiResources<Installation>(
41+
selectedNamespaces,
42+
InstallationsRequest,
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+
) => {
61+
const selectedItems = Array.from(e.detail.items || []);
62+
const selectedValues = selectedItems
63+
.map((item) => item.text)
64+
.filter((text): text is string => typeof text === 'string');
65+
66+
setSelectedNamespaces(selectedValues);
67+
};
68+
69+
const getStatusSymbol = (phase?: string) => {
70+
if (!phase) return '⚪';
71+
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,
99+
);
100+
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[]) || [];
111+
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))
121+
) : (
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+
</>
148+
)}
149+
</TreeItem>
150+
);
151+
};
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+
163+
return (
164+
<>
165+
{namespaces && (
166+
<MultiComboBox
167+
placeholder={t('Landscapers.multiComboBoxPlaceholder')}
168+
style={{ marginBottom: '1rem' }}
169+
onSelectionChange={handleSelectionChange}
170+
>
171+
{namespaces.map((ns) => (
172+
<MultiComboBoxItem key={ns.metadata.name} text={ns.metadata.name} />
173+
))}
174+
</MultiComboBox>
175+
)}
176+
<Tree>{rootInstallations.map((inst) => renderTreeItems(inst))}</Tree>
177+
</>
178+
);
179+
}

src/context/ToastContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const ToastProvider: FC<{ children: ReactNode }> = ({ children }) => {
3535
setTimeout(() => {
3636
setToastContent({
3737
text: message,
38-
duration: duration || 3000,
38+
duration: duration || 8000,
3939
});
4040
setToastVisible(true);
4141
}, 100);

src/lib/api/types/landscaper/hooks.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { Resource } from '../resource';
22

3-
interface GraphDeployItemsType {
3+
export interface DeployItem {
4+
objectName: string;
45
metadata: {
56
name: string;
67
namespace: string;
78
uid: string;
8-
ownerReferences: {
9-
uid: string;
10-
}[];
9+
ownerReferences: { uid: string }[];
1110
};
1211
status: {
1312
phase: string;
1413
};
1514
}
1615

17-
export const ListGraphDeployItems: Resource<GraphDeployItemsType[]> = {
18-
path: '/apis/landscaper.gardener.cloud/v1alpha1/deployitems',
19-
jq: '[.items[] | {metadata: .metadata | {name, namespace, uid, ownerReferences: (try [{uid: .ownerReferences[].uid}] catch [])}, status: .status | {phase}}]',
20-
};
16+
export interface DeployItemsListResponse {
17+
items: DeployItem[];
18+
}
19+
20+
export const DeployItemsRequest = (
21+
namespace: string,
22+
): Resource<DeployItemsListResponse> => ({
23+
path: `/apis/landscaper.gardener.cloud/v1alpha1/namespaces/${namespace}/deployitems`,
24+
});
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import { Resource } from '../resource';
22

3-
interface GraphExecutionsType {
3+
export interface Execution {
4+
objectName: string;
45
metadata: {
56
name: string;
67
namespace: string;
78
uid: string;
8-
ownerReferences: {
9-
uid: string;
10-
}[];
119
};
12-
status: {
13-
phase: string;
10+
status?: {
11+
phase?: string;
12+
deployItemCache?: {
13+
activeDIs?: {
14+
objectName: string;
15+
}[];
16+
};
1417
};
1518
}
1619

17-
export const ListGraphExecutions: Resource<GraphExecutionsType[]> = {
18-
path: '/apis/landscaper.gardener.cloud/v1alpha1/executions',
19-
jq: '[.items[] | {metadata: .metadata | {name, namespace, uid, ownerReferences: (try [{uid: .ownerReferences[].uid}] catch [])}, status: .status | {phase}}]',
20-
};
20+
export interface ExecutionsListResponse {
21+
items: Execution[];
22+
}
23+
24+
export const ExecutionsRequest = (
25+
namespace: string,
26+
): Resource<ExecutionsListResponse> => ({
27+
path: `/apis/landscaper.gardener.cloud/v1alpha1/namespaces/${namespace}/executions`,
28+
});
Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,32 @@
11
import { Resource } from '../resource';
22

3-
interface GraphInstallationsType {
3+
export interface Installation {
4+
objectName: string;
45
metadata: {
56
name: string;
67
namespace: string;
78
uid: string;
8-
ownerReferences: {
9-
uid: string;
10-
}[];
119
};
12-
status: {
13-
phase: string;
10+
status?: {
11+
phase?: string;
12+
executionRef?: {
13+
name: string;
14+
namespace: string;
15+
};
16+
subInstCache?: {
17+
activeSubs?: {
18+
objectName: string;
19+
}[];
20+
};
1421
};
1522
}
1623

17-
export const ListGraphInstallations: Resource<GraphInstallationsType[]> = {
18-
path: '/apis/landscaper.gardener.cloud/v1alpha1/installations',
19-
jq: '[.items[] | {metadata: .metadata | {name, namespace, uid, ownerReferences: (try [{uid: .ownerReferences[].uid}] catch [])}, status: .status | {phase}}]',
20-
};
21-
22-
interface TableInstallationsType {
23-
metadata: {
24-
name: string;
25-
namespace: string;
26-
creationTimestamp: string;
27-
};
28-
status: {
29-
phase: string;
30-
};
24+
export interface InstallationsListResponse {
25+
items: Installation[];
3126
}
3227

33-
export const ListTableInstallations: Resource<TableInstallationsType[]> = {
34-
path: '/apis/landscaper.gardener.cloud/v1alpha1/installations',
35-
jq: '[.items[] | {metadata: .metadata | {name, namespace, creationTimestamp}, status: .status | {phase}}]',
36-
};
28+
export const InstallationsRequest = (
29+
namespace: string,
30+
): Resource<InstallationsListResponse> => ({
31+
path: `/apis/landscaper.gardener.cloud/v1alpha1/namespaces/${namespace}/installations`,
32+
});

0 commit comments

Comments
 (0)