From 6574533092cafd171f4b9c9649e8c0c8f2805d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20W=C3=BCrbach?= Date: Mon, 17 Jun 2024 23:02:13 +0200 Subject: [PATCH] fix: improved status rendering --- .changeset/tender-suns-impress.md | 6 + examples/entities.yaml | 36 ++++- .../src/clients/fetch-app-info.test.ts | 130 ++++++++++++++++++ .../src/clients/fetch-app-info.ts | 41 +++++- .../HumanitecCardComponent.test.tsx | 120 +++++++++++++++- .../src/components/HumanitecCardContent.tsx | 40 ++++-- 6 files changed, 348 insertions(+), 25 deletions(-) create mode 100644 .changeset/tender-suns-impress.md create mode 100644 plugins/humanitec-common/src/clients/fetch-app-info.test.ts diff --git a/.changeset/tender-suns-impress.md b/.changeset/tender-suns-impress.md new file mode 100644 index 00000000..b5c1b164 --- /dev/null +++ b/.changeset/tender-suns-impress.md @@ -0,0 +1,6 @@ +--- +'@humanitec/backstage-plugin-common': patch +'@humanitec/backstage-plugin': patch +--- + +fix: improved status rendering diff --git a/examples/entities.yaml b/examples/entities.yaml index 63d7e5d8..95f2aea9 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -11,10 +11,10 @@ spec: apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: example-service + name: empty annotations: "humanitec.com/orgId": "humanitec-backstage-plugins" - "humanitec.com/appId": "example-service" + "humanitec.com/appId": "empty" spec: type: service lifecycle: experimental @@ -26,7 +26,37 @@ spec: apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: example-service-without + name: deployed + annotations: + "humanitec.com/orgId": "humanitec-backstage-plugins" + "humanitec.com/appId": "deployed" +spec: + type: service + lifecycle: experimental + owner: guests + system: examples + providesApis: [example-grpc-api] +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: partially-deployed + annotations: + "humanitec.com/orgId": "humanitec-backstage-plugins" + "humanitec.com/appId": "partially-deployed" +spec: + type: service + lifecycle: experimental + owner: guests + system: examples + providesApis: [example-grpc-api] +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: missing-humanitec-annotations spec: type: service lifecycle: experimental diff --git a/plugins/humanitec-common/src/clients/fetch-app-info.test.ts b/plugins/humanitec-common/src/clients/fetch-app-info.test.ts new file mode 100644 index 00000000..410e7f4b --- /dev/null +++ b/plugins/humanitec-common/src/clients/fetch-app-info.test.ts @@ -0,0 +1,130 @@ +import { fetchAppInfo, FetchAppInfoClient } from './fetch-app-info'; + +type envs = Awaited> +type resources = Awaited> +type runtime = Awaited> + +describe('fetchAppInfo', () => { + const createClientMock = ({ + envs, + resources, + runtime + }: { + envs: envs, + resources?: resources, + runtime?: runtime + }) => { + return { + getEnvironments: jest.fn().mockResolvedValue(envs), + getActiveEnvironmentResources: jest.fn().mockResolvedValue(resources), + getRuntimeInfo: jest.fn().mockResolvedValue(runtime), + }; + } + + const deployedEnv = () => ({ + type: 'test', + id: 'test', + name: 'test', + last_deploy: { + id: 'test', + created_at: 'test', + status: 'succeeded' as 'succeeded', + created_by: 'test', + comment: 'test', + env_id: 'test', + export_file: 'test', + from_id: 'test', + export_status: 'test', + set_id: 'test', + status_changed_at: 'test', + } + }) + const k8sClusterResource = (clusterType: string) => ({ + app_id: 'test', + def_id: 'test', + env_id: 'test', + env_type: 'test', + org_id: 'test', + res_id: 'k8s-cluster', + resource: { + cluster_type: clusterType, + }, + status: 'active' as 'active', + type: 'test', + updated_at: 'test', + }) + + it('without environments', async () => { + const client = createClientMock({envs: []}) + const res = await fetchAppInfo({ client }, 'appId') + + expect(res).toEqual([]) + + expect(client.getEnvironments).toHaveBeenCalledWith('appId') + expect(client.getActiveEnvironmentResources).not.toHaveBeenCalled() + expect(client.getRuntimeInfo).not.toHaveBeenCalled() + }) + + + it('without a deployment', async () => { + const env = { + type: 'test', + id: 'test', + name: 'test', + } + const client = createClientMock({envs: [env]}) + + const res = await fetchAppInfo({ client }, 'appId') + + expect(res).toEqual([{ + ...env, + usesGitCluster: false, + runtime: null, + resources: [] + }]) + + expect(client.getEnvironments).toHaveBeenCalledWith('appId') + expect(client.getActiveEnvironmentResources).not.toHaveBeenCalled() + expect(client.getRuntimeInfo).not.toHaveBeenCalled() + }) + + it('with a git resource', async () => { + const env = deployedEnv() + const gitClusterResource = k8sClusterResource('git') + const client = createClientMock({envs: [env], resources: [gitClusterResource]}) + + const res = await fetchAppInfo({ client }, 'appId') + + expect(res).toEqual([{ + ...env, + usesGitCluster: true, + runtime: null, + resources: [gitClusterResource] + }]) + expect(client.getEnvironments).toHaveBeenCalledWith('appId') + expect(client.getActiveEnvironmentResources).toHaveBeenCalledWith('appId', 'test') + expect(client.getRuntimeInfo).not.toHaveBeenCalled() + }) + + it('without a k8s-cluster git resource', async () => { + const env = deployedEnv() + const gkeClusterResource = k8sClusterResource('gke') + const runtime = { + namespace: 'test', + modules: {} + } + const client = createClientMock({envs: [env], resources: [gkeClusterResource], runtime }) + + const res = await fetchAppInfo({ client }, 'appId') + + expect(res).toEqual([{ + ...env, + usesGitCluster: false, + runtime, + resources: [gkeClusterResource] + }]) + expect(client.getEnvironments).toHaveBeenCalledWith('appId') + expect(client.getActiveEnvironmentResources).toHaveBeenCalledWith('appId', 'test') + expect(client.getRuntimeInfo).toHaveBeenCalledWith('appId', 'test') + }) +}); diff --git a/plugins/humanitec-common/src/clients/fetch-app-info.ts b/plugins/humanitec-common/src/clients/fetch-app-info.ts index d429f91f..0cef6660 100644 --- a/plugins/humanitec-common/src/clients/fetch-app-info.ts +++ b/plugins/humanitec-common/src/clients/fetch-app-info.ts @@ -1,16 +1,45 @@ import { HumanitecClient } from './humanitec'; -export async function fetchAppInfo({ client }: { client: HumanitecClient; }, appId: string) { +const k8sResID = 'k8s-cluster'; +const gitClusterType = 'git'; + +export type FetchAppInfoClient = Pick; + +export async function fetchAppInfo({ client }: { client: FetchAppInfoClient; }, appId: string) { const environments = await client.getEnvironments(appId); return await Promise.all(environments.map(async (env) => { - const [runtime, resources] = await Promise.all([ - client.getRuntimeInfo(appId, env.id), - client.getActiveEnvironmentResources(appId, env.id), - ]); + let usesGitCluster = false + if (!env.last_deploy) { + return { + ...env, + usesGitCluster, + runtime: null, + resources: [] + }; + } + + const resources = await client.getActiveEnvironmentResources(appId, env.id); + + // k8s-cluster of cluster_type git have no runtime information + for (const resource of resources) { + if (resource.res_id === k8sResID && resource.resource?.cluster_type === gitClusterType) { + usesGitCluster = true; + + return { + ...env, + usesGitCluster, + runtime: null, + resources + }; + } + } + + const runtime = await client.getRuntimeInfo(appId, env.id) return { ...env, + usesGitCluster, runtime, resources }; @@ -18,4 +47,4 @@ export async function fetchAppInfo({ client }: { client: HumanitecClient; }, app } export type FetchAppInfoResponse = Awaited> -export type FetchAppInfoEnvironment = FetchAppInfoResponse[0] \ No newline at end of file +export type FetchAppInfoEnvironment = FetchAppInfoResponse[0] diff --git a/plugins/humanitec/src/components/HumanitecCardComponent.test.tsx b/plugins/humanitec/src/components/HumanitecCardComponent.test.tsx index b9fd85a7..9fc47eb5 100644 --- a/plugins/humanitec/src/components/HumanitecCardComponent.test.tsx +++ b/plugins/humanitec/src/components/HumanitecCardComponent.test.tsx @@ -1,7 +1,125 @@ import { Entity } from '@backstage/catalog-model'; -import { hasHumanitecAnnotations } from './HumanitecCardComponent'; +import { + hasHumanitecAnnotations, + HumanitecCardComponent, +} from './HumanitecCardComponent'; + +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { rootRouteRef } from '../routes'; +import { + configApiRef, + discoveryApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; + +let originalAppInfo = false; + +jest.mock('../hooks/useAppInfo', () => ({ + useAppInfo: jest.fn((...args) => { + if (originalAppInfo) { + const { useAppInfo } = jest.requireActual('../hooks/useAppInfo'); + return useAppInfo(args[0]); + } + return [{}]; + }), +})); describe('', () => { + beforeEach(() => { + originalAppInfo = false; + }); + + it('renders a warning without annotations', async () => { + originalAppInfo = true; + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + description: 'This is the description', + annotations: {}, + }, + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/create': rootRouteRef, + }, + }, + ); + expect( + screen.getByText( + 'No Humanitec annotations defined for this entity. You can add annotations to entity YAML as shown in the highlighted example below:', + ), + ).toBeInTheDocument(); + }); + + it('renders never deployed environments', async () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'humanitec.com/orgId': 'orgId', + 'humanitec.com/appId': 'appId', + }, + }, + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/create': rootRouteRef, + }, + }, + ); + expect(screen.getByText('Never deployed')).toBeInTheDocument(); + }); + it('returns hasHumanitecAnnotations truthy if the entity has humanitec annotations', async () => { const entity: Entity = { apiVersion: 'v1', diff --git a/plugins/humanitec/src/components/HumanitecCardContent.tsx b/plugins/humanitec/src/components/HumanitecCardContent.tsx index 71467d6d..7ab690d6 100644 --- a/plugins/humanitec/src/components/HumanitecCardContent.tsx +++ b/plugins/humanitec/src/components/HumanitecCardContent.tsx @@ -28,12 +28,14 @@ export function HumanitecCardContent({ const env = environments.find(e => e.id === selectedEnv); const resources = (selectedWorkload && - env?.resources.filter(resource => - resource.res_id.startsWith(`modules.${selectedWorkload}`), - )) || + env?.resources + ?.filter(resource => + resource.res_id.startsWith(`modules.${selectedWorkload}`), + ) + .filter(resource => resource.type !== 'workload')) || []; const workloads = - env?.resources.filter(resource => resource.type === 'workload') || []; + env?.resources?.filter(resource => resource.type === 'workload') || []; return ( <> @@ -91,6 +93,11 @@ export function HumanitecCardContent({ No workloads reported. )} + {env && env.usesGitCluster && ( + + No runtime information available for cluster type "git". + + )} ) @@ -101,17 +108,20 @@ export function HumanitecCardContent({ Resources - {resources - .filter(resource => resource.type !== 'workload') - .map(resource => ( - - ))} + {resources.map(resource => ( + + ))} + {resources.length === 0 && ( + + No resources reported. + + )} ) : null}