Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,22 @@
"accessError": "Managed Control Plane does not have access information yet",
"componentsTitle": "Components",
"crossplaneTitle": "Crossplane",
"gitOpsTitle": "GitOps"
"gitOpsTitle": "GitOps",
"landscapersTitle": "Landscapers"
},
"ToastContext": {
"errorMessage": "useToast must be used within a ToastProvider"
},
"Landscapers": {
"headerLandscapers": "Landscapers",
"multiComboBoxPlaceholder": "Select namespace",
"noItemsFound": "No Deploy Items found",
"deployItems": "Deploy Items",
"treeDeployItem": "Deploy Item",
"treeInstallation": "Installation",
"treeExecution": "Execution",
"noExecutionFound": "No Executions found"
},
"CopyButton": {
"copiedMessage": "Copied To Clipboard",
"failedMessage": "Failed to copy"
Expand Down
179 changes: 179 additions & 0 deletions src/components/ControlPlane/Landscapers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { useTranslation } from 'react-i18next';
import {
MultiComboBox,
MultiComboBoxDomRef,
MultiComboBoxItem,
Tree,
TreeItem,
Ui5CustomEvent,
} from '@ui5/webcomponents-react';
import { useState, JSX } from 'react';
import { resourcesInterval } from '../../lib/shared/constants';
import useResource, {
useMultipleApiResources,
} from '../../lib/api/useApiResource';
import { ListNamespaces } from '../../lib/api/types/k8s/listNamespaces';
import {
Installation,
InstallationsRequest,
} from '../../lib/api/types/landscaper/listInstallations';
import {
Execution,
ExecutionsRequest,
} from '../../lib/api/types/landscaper/listExecutions';
import {
DeployItem,
DeployItemsRequest,
} from '../../lib/api/types/landscaper/listDeployItems';

import { MultiComboBoxSelectionChangeEventDetail } from '@ui5/webcomponents/dist/MultiComboBox.js';

export function Landscapers() {
const { t } = useTranslation();

const { data: namespaces } = useResource(ListNamespaces, {
refreshInterval: resourcesInterval,
});

const [selectedNamespaces, setSelectedNamespaces] = useState<string[]>([]);

const { data: installations = [] } = useMultipleApiResources<Installation>(
selectedNamespaces,
InstallationsRequest,
);

const { data: executions = [] } = useMultipleApiResources<Execution>(
selectedNamespaces,
ExecutionsRequest,
);

const { data: deployItems = [] } = useMultipleApiResources<DeployItem>(
selectedNamespaces,
DeployItemsRequest,
);

const handleSelectionChange = (
e: Ui5CustomEvent<
MultiComboBoxDomRef,
MultiComboBoxSelectionChangeEventDetail
>,
) => {
const selectedItems = Array.from(e.detail.items || []);
const selectedValues = selectedItems
.map((item) => item.text)
.filter((text): text is string => typeof text === 'string');

setSelectedNamespaces(selectedValues);
};

const getStatusSymbol = (phase?: string) => {
if (!phase) return '⚪';

const phaseLower = phase.toLowerCase();

if (phaseLower === 'succeeded') {
return '✅';
} else if (phaseLower === 'failed') {
return '❌';
}

return '⚪';
};

const renderTreeItems = (installation: Installation): JSX.Element => {
const subInstallations =
(installation.status?.subInstCache?.activeSubs
?.map((sub) =>
installations.find(
(i) =>
i.metadata.name === sub.objectName &&
i.metadata.namespace === installation.metadata.namespace,
),
)
.filter(Boolean) as Installation[]) || [];

const execution = executions.find(
(e) =>
e.metadata.name === installation.status?.executionRef?.name &&
e.metadata.namespace === installation.status?.executionRef?.namespace,
);

const relatedDeployItems =
(execution?.status?.deployItemCache?.activeDIs
?.map((di) =>
deployItems.find(
(item) =>
item.metadata.name === di.objectName &&
item.metadata.namespace === execution.metadata.namespace,
),
)
.filter(Boolean) as DeployItem[]) || [];

return (
<TreeItem
key={installation.metadata.uid}
text={`${getStatusSymbol(installation.status?.phase)} ${t(
'Landscapers.treeInstallation',
)}: ${installation.metadata.name} (${installation.status?.phase || '-'})`}
>
{subInstallations.length > 0 ? (
subInstallations.map((sub) => renderTreeItems(sub))
) : (
<>
<TreeItem
text={`${
execution
? `${getStatusSymbol(execution.status?.phase)} ${t(
'Landscapers.treeExecution',
)}: ${execution.metadata.name} (${execution.status?.phase || '-'})`
: t('Landscapers.noExecutionFound')
}`}
/>

<TreeItem text={t('Landscapers.deployItems')}>
{relatedDeployItems.length > 0 ? (
relatedDeployItems.map((di) => (
<TreeItem
key={di.metadata.uid}
text={`${getStatusSymbol(di.status?.phase)} ${t(
'Landscapers.treeDeployItem',
)}: ${di.metadata.name} (${di.status?.phase || '-'})`}
/>
))
) : (
<TreeItem text={t('Landscapers.noItemsFound')} />
)}
</TreeItem>
</>
)}
</TreeItem>
);
};

const rootInstallations = installations.filter((inst) => {
return !installations.some((parent) =>
parent.status?.subInstCache?.activeSubs?.some(
(sub: { objectName: string }) =>
sub.objectName === inst.metadata.name &&
parent.metadata.namespace === inst.metadata.namespace,
),
);
});

return (
<>
{namespaces && (
<MultiComboBox
placeholder={t('Landscapers.multiComboBoxPlaceholder')}
style={{ marginBottom: '1rem' }}
onSelectionChange={handleSelectionChange}
>
{namespaces.map((ns) => (
<MultiComboBoxItem key={ns.metadata.name} text={ns.metadata.name} />
))}
</MultiComboBox>
)}
<Tree>{rootInstallations.map((inst) => renderTreeItems(inst))}</Tree>
</>
);
}
55 changes: 0 additions & 55 deletions src/lib/api/types/landscaper/hooks.ts

This file was deleted.

20 changes: 12 additions & 8 deletions src/lib/api/types/landscaper/listDeployItems.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { Resource } from '../resource';

interface GraphDeployItemsType {
export interface DeployItem {
objectName: string;
metadata: {
name: string;
namespace: string;
uid: string;
ownerReferences: {
uid: string;
}[];
ownerReferences: { uid: string }[];
};
status: {
phase: string;
};
}

export const ListGraphDeployItems: Resource<GraphDeployItemsType[]> = {
path: '/apis/landscaper.gardener.cloud/v1alpha1/deployitems',
jq: '[.items[] | {metadata: .metadata | {name, namespace, uid, ownerReferences: (try [{uid: .ownerReferences[].uid}] catch [])}, status: .status | {phase}}]',
};
export interface DeployItemsListResponse {
items: DeployItem[];
}

export const DeployItemsRequest = (
namespace: string,
): Resource<DeployItemsListResponse> => ({
path: `/apis/landscaper.gardener.cloud/v1alpha1/namespaces/${namespace}/deployitems`,
});
28 changes: 18 additions & 10 deletions src/lib/api/types/landscaper/listExecutions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { Resource } from '../resource';

interface GraphExecutionsType {
export interface Execution {
objectName: string;
metadata: {
name: string;
namespace: string;
uid: string;
ownerReferences: {
uid: string;
}[];
};
status: {
phase: string;
status?: {
phase?: string;
deployItemCache?: {
activeDIs?: {
objectName: string;
}[];
};
};
}

export const ListGraphExecutions: Resource<GraphExecutionsType[]> = {
path: '/apis/landscaper.gardener.cloud/v1alpha1/executions',
jq: '[.items[] | {metadata: .metadata | {name, namespace, uid, ownerReferences: (try [{uid: .ownerReferences[].uid}] catch [])}, status: .status | {phase}}]',
};
export interface ExecutionsListResponse {
items: Execution[];
}

export const ExecutionsRequest = (
namespace: string,
): Resource<ExecutionsListResponse> => ({
path: `/apis/landscaper.gardener.cloud/v1alpha1/namespaces/${namespace}/executions`,
});
44 changes: 20 additions & 24 deletions src/lib/api/types/landscaper/listInstallations.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,32 @@
import { Resource } from '../resource';

interface GraphInstallationsType {
export interface Installation {
objectName: string;
metadata: {
name: string;
namespace: string;
uid: string;
ownerReferences: {
uid: string;
}[];
};
status: {
phase: string;
status?: {
phase?: string;
executionRef?: {
name: string;
namespace: string;
};
subInstCache?: {
activeSubs?: {
objectName: string;
}[];
};
};
}

export const ListGraphInstallations: Resource<GraphInstallationsType[]> = {
path: '/apis/landscaper.gardener.cloud/v1alpha1/installations',
jq: '[.items[] | {metadata: .metadata | {name, namespace, uid, ownerReferences: (try [{uid: .ownerReferences[].uid}] catch [])}, status: .status | {phase}}]',
};

interface TableInstallationsType {
metadata: {
name: string;
namespace: string;
creationTimestamp: string;
};
status: {
phase: string;
};
export interface InstallationsListResponse {
items: Installation[];
}

export const ListTableInstallations: Resource<TableInstallationsType[]> = {
path: '/apis/landscaper.gardener.cloud/v1alpha1/installations',
jq: '[.items[] | {metadata: .metadata | {name, namespace, creationTimestamp}, status: .status | {phase}}]',
};
export const InstallationsRequest = (
namespace: string,
): Resource<InstallationsListResponse> => ({
path: `/apis/landscaper.gardener.cloud/v1alpha1/namespaces/${namespace}/installations`,
});
Loading
Loading