diff --git a/plugins/kubernetes/src/components/Cluster/Cluster.test.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.test.tsx new file mode 100644 index 0000000000..ddadd95272 --- /dev/null +++ b/plugins/kubernetes/src/components/Cluster/Cluster.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { Cluster } from './Cluster'; + +jest.mock('../../hooks'); +import * as oneDeployment from '../../__fixtures__/1-deployments.json'; + +describe('Cluster', () => { + it('render 1 cluster', async () => { + const { getByText } = render( + wrapInTestApp( + (), + } as any)} + />, + ), + ); + + expect(getByText('cluster-1')).toBeInTheDocument(); + expect(getByText('10 pods')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.tsx similarity index 52% rename from plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx rename to plugins/kubernetes/src/components/Cluster/Cluster.tsx index 260c77ed9e..d238396f53 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/Cluster/Cluster.tsx @@ -23,32 +23,20 @@ import { Grid, Typography, } from '@material-ui/core'; -import { Entity } from '@backstage/catalog-model'; import { ClusterObjects } from '@backstage/plugin-kubernetes-common'; -import { ErrorPanel } from './ErrorPanel'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { DeploymentsAccordions } from '../DeploymentsAccordions'; -import { ErrorReporting } from '../ErrorReporting'; import { groupResponses } from '../../utils/response'; -import { DetectedError, detectErrors } from '../../error-detection'; import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; import { CustomResources } from '../CustomResources'; -import EmptyStateImage from '../../assets/emptystate.svg'; import { ClusterContext, GroupedResponsesContext, PodNamesWithErrorsContext, - useKubernetesObjects, } from '../../hooks'; -import { - Content, - Page, - Progress, - StatusError, - StatusOK, -} from '@backstage/core-components'; +import { StatusError, StatusOK } from '@backstage/core-components'; type ClusterSummaryProps = { clusterName: string; @@ -117,7 +105,7 @@ type ClusterProps = { children?: React.ReactNode; }; -const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { +export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { const groupedResponses = groupResponses(clusterObjects.resources); return ( @@ -153,107 +141,3 @@ const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { ); }; - -type KubernetesContentProps = { entity: Entity; children?: React.ReactNode }; - -export const KubernetesContent = ({ entity }: KubernetesContentProps) => { - const { kubernetesObjects, error } = useKubernetesObjects(entity); - - const clustersWithErrors = - kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? []; - - const detectedErrors = - kubernetesObjects !== undefined - ? detectErrors(kubernetesObjects) - : new Map(); - - return ( - - - {kubernetesObjects === undefined && error === undefined && } - - {/* errors retrieved from the kubernetes clusters */} - {clustersWithErrors.length > 0 && ( - - - - - - )} - - {/* other errors */} - {error !== undefined && ( - - - - - - )} - - {kubernetesObjects && ( - - - - - - - - - Your Clusters - - - {kubernetesObjects?.items.length <= 0 && ( - - - - No resources on any known clusters for{' '} - {entity.metadata.name} - - - - EmptyState - - - )} - {kubernetesObjects?.items.length > 0 && - kubernetesObjects?.items.map((item, i) => { - const podsWithErrors = new Set( - detectedErrors - .get(item.cluster.name) - ?.filter(de => de.kind === 'Pod') - .map(de => de.names) - .flat() ?? [], - ); - - return ( - - - - ); - })} - - - )} - - - ); -}; diff --git a/plugins/kubernetes/src/components/KubernetesContent/index.ts b/plugins/kubernetes/src/components/Cluster/index.ts similarity index 91% rename from plugins/kubernetes/src/components/KubernetesContent/index.ts rename to plugins/kubernetes/src/components/Cluster/index.ts index 1a77bd40b2..dc9df63d42 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/index.ts +++ b/plugins/kubernetes/src/components/Cluster/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { KubernetesContent } from './KubernetesContent'; +export { Cluster } from './Cluster'; diff --git a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.test.tsx b/plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.test.tsx similarity index 100% rename from plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.test.tsx rename to plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.test.tsx diff --git a/plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx b/plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.tsx similarity index 100% rename from plugins/kubernetes/src/components/KubernetesContent/ErrorPanel.tsx rename to plugins/kubernetes/src/components/ErrorPanel/ErrorPanel.tsx diff --git a/plugins/kubernetes/src/components/ErrorPanel/index.ts b/plugins/kubernetes/src/components/ErrorPanel/index.ts new file mode 100644 index 0000000000..2d3778e2c2 --- /dev/null +++ b/plugins/kubernetes/src/components/ErrorPanel/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ErrorPanel } from './ErrorPanel'; diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.test.tsx b/plugins/kubernetes/src/components/KubernetesContent.test.tsx similarity index 95% rename from plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.test.tsx rename to plugins/kubernetes/src/components/KubernetesContent.test.tsx index cd4b902f5f..db8cd06782 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.test.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent.test.tsx @@ -18,11 +18,11 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { KubernetesContent } from './KubernetesContent'; -import { useKubernetesObjects } from '../../hooks'; +import { useKubernetesObjects } from '../hooks'; -jest.mock('../../hooks'); -import * as oneDeployment from '../../__fixtures__/1-deployments.json'; -import * as twoDeployments from '../../__fixtures__/2-deployments.json'; +jest.mock('../hooks'); +import * as oneDeployment from '../__fixtures__/1-deployments.json'; +import * as twoDeployments from '../__fixtures__/2-deployments.json'; describe('KubernetesContent', () => { it('render empty response', async () => { diff --git a/plugins/kubernetes/src/components/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent.tsx new file mode 100644 index 0000000000..036bcdb608 --- /dev/null +++ b/plugins/kubernetes/src/components/KubernetesContent.tsx @@ -0,0 +1,130 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Divider, Grid, Typography } from '@material-ui/core'; +import { Entity } from '@backstage/catalog-model'; +import { ErrorPanel } from './ErrorPanel'; +import { ErrorReporting } from './ErrorReporting'; +import { DetectedError, detectErrors } from '../error-detection'; +import { Cluster } from './Cluster'; +import EmptyStateImage from '../assets/emptystate.svg'; +import { useKubernetesObjects } from '../hooks'; +import { Content, Page, Progress } from '@backstage/core-components'; + +type KubernetesContentProps = { entity: Entity; children?: React.ReactNode }; + +export const KubernetesContent = ({ entity }: KubernetesContentProps) => { + const { kubernetesObjects, error } = useKubernetesObjects(entity); + + const clustersWithErrors = + kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? []; + + const detectedErrors = + kubernetesObjects !== undefined + ? detectErrors(kubernetesObjects) + : new Map(); + + return ( + + + {kubernetesObjects === undefined && error === undefined && } + + {/* errors retrieved from the kubernetes clusters */} + {clustersWithErrors.length > 0 && ( + + + + + + )} + + {/* other errors */} + {error !== undefined && ( + + + + + + )} + + {kubernetesObjects && ( + + + + + + + + + Your Clusters + + + {kubernetesObjects?.items.length <= 0 && ( + + + + No resources on any known clusters for{' '} + {entity.metadata.name} + + + + EmptyState + + + )} + {kubernetesObjects?.items.length > 0 && + kubernetesObjects?.items.map((item, i) => { + const podsWithErrors = new Set( + detectedErrors + .get(item.cluster.name) + ?.filter(de => de.kind === 'Pod') + .map(de => de.names) + .flat() ?? [], + ); + + return ( + + + + ); + })} + + + )} + + + ); +}; diff --git a/plugins/kubernetes/src/hooks/useKubernetesObjects.ts b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts index 4f19816296..51df92e8d2 100644 --- a/plugins/kubernetes/src/hooks/useKubernetesObjects.ts +++ b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts @@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { kubernetesApiRef } from '../api/types'; import { kubernetesAuthProvidersApiRef } from '../kubernetes-auth-provider/types'; import { useEffect, useState } from 'react'; +import { useInterval } from 'react-use'; import { KubernetesRequestBody, ObjectsByEntityResponse, @@ -38,51 +39,56 @@ export const useKubernetesObjects = (entity: Entity): KubernetesObjects => { const [error, setError] = useState(undefined); + const getObjects = async () => { + let clusters = []; + + try { + clusters = await kubernetesApi.getClusters(); + } catch (e) { + setError(e.message); + return; + } + + const authProviders: string[] = [ + ...new Set(clusters.map(c => c.authProvider)), + ]; + // For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider + let requestBody: KubernetesRequestBody = { + entity, + }; + for (const authProviderStr of authProviders) { + // Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously + try { + requestBody = + await kubernetesAuthProvidersApi.decorateRequestBodyForAuth( + authProviderStr, + requestBody, + ); + } catch (e) { + setError(e.message); + return; + } + } + + try { + setKubernetesObjects(await kubernetesApi.getObjectsByEntity(requestBody)); + } catch (e) { + setError(e.message); + return; + } + }; + useEffect(() => { - (async () => { - let clusters = []; - - try { - clusters = await kubernetesApi.getClusters(); - } catch (e) { - setError(e.message); - return; - } - - const authProviders: string[] = [ - ...new Set(clusters.map(c => c.authProvider)), - ]; - // For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider - let requestBody: KubernetesRequestBody = { - entity, - }; - for (const authProviderStr of authProviders) { - // Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously - try { - requestBody = - await kubernetesAuthProvidersApi.decorateRequestBodyForAuth( - authProviderStr, - requestBody, - ); - } catch (e) { - setError(e.message); - return; - } - } - - try { - setKubernetesObjects( - await kubernetesApi.getObjectsByEntity(requestBody), - ); - } catch (e) { - setError(e.message); - return; - } - })(); + getObjects(); /* eslint-disable react-hooks/exhaustive-deps */ }, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]); /* eslint-enable react-hooks/exhaustive-deps */ + useInterval(() => { + getObjects(); + // TODO make configurable + }, 10000); + return { kubernetesObjects, error,