Skip to content

Commit

Permalink
Merge pull request #28 from humanitec/handle400
Browse files Browse the repository at this point in the history
fix: improve status rendering
  • Loading branch information
damien-duignan authored Jun 18, 2024
2 parents 3a30c90 + 6574533 commit 2e5eb89
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 25 deletions.
6 changes: 6 additions & 0 deletions .changeset/tender-suns-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@humanitec/backstage-plugin-common': patch
'@humanitec/backstage-plugin': patch
---

fix: improved status rendering
36 changes: 33 additions & 3 deletions examples/entities.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
130 changes: 130 additions & 0 deletions plugins/humanitec-common/src/clients/fetch-app-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { fetchAppInfo, FetchAppInfoClient } from './fetch-app-info';

type envs = Awaited<ReturnType<FetchAppInfoClient["getEnvironments"]>>
type resources = Awaited<ReturnType<FetchAppInfoClient["getActiveEnvironmentResources"]>>
type runtime = Awaited<ReturnType<FetchAppInfoClient["getRuntimeInfo"]>>

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')
})
});
41 changes: 35 additions & 6 deletions plugins/humanitec-common/src/clients/fetch-app-info.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
import { HumanitecClient } from './humanitec';

export async function fetchAppInfo({ client }: { client: HumanitecClient; }, appId: string) {
const k8sResID = 'k8s-cluster';
const gitClusterType = 'git';

export type FetchAppInfoClient = Pick<HumanitecClient, 'getEnvironments' | 'getActiveEnvironmentResources' | 'getRuntimeInfo'>;

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
};
}));
}

export type FetchAppInfoResponse = Awaited<ReturnType<typeof fetchAppInfo>>
export type FetchAppInfoEnvironment = FetchAppInfoResponse[0]
export type FetchAppInfoEnvironment = FetchAppInfoResponse[0]
120 changes: 119 additions & 1 deletion plugins/humanitec/src/components/HumanitecCardComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -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('<HumanitecCardComponent />', () => {
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(
<TestApiProvider
apis={[
[configApiRef, {}],
[discoveryApiRef, {}],
[
identityApiRef,
{
getCredentials: jest.fn().mockResolvedValue({}),
},
],
]}
>
<EntityProvider entity={entity}>
<HumanitecCardComponent />
</EntityProvider>
</TestApiProvider>,
{
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(
<TestApiProvider
apis={[
[configApiRef, {}],
[
discoveryApiRef,
{
getBaseUrl: jest
.fn()
.mockResolvedValue('https://app.humanitec.io'),
},
],
[
identityApiRef,
{
getCredentials: jest.fn().mockResolvedValue({}),
},
],
]}
>
<EntityProvider entity={entity}>
<HumanitecCardComponent />
</EntityProvider>
</TestApiProvider>,
{
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',
Expand Down
Loading

0 comments on commit 2e5eb89

Please sign in to comment.